@happyvertical/smrt-subscriptions 0.57.35 → 0.57.37

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 CHANGED
@@ -84,6 +84,7 @@ that reverted to DECIMAL passes every SQLite suite.
84
84
  caller-named other version needs an authorized replacement.
85
85
  - `canceled` subscriptions entitle until `currentPeriodEnd` only with
86
86
  `retainsPaidPeriod`, derived on save from an entitled pre-cancel row.
87
+ - **Recipe `subscriptions.plans`** (`src/recipes.ts`, help in `src/plans.recipe.md`): lists plans, subscriptions, plan versions (read-only, written by `plan-versioning.ts`) and charges (read-only, written by the usage service) for the planner and other catalog hosts. Fields it shows carry `@field({ description })` in plain words, money fields saying "in cents"; keep them when editing the models. Reseller models and pricing rules stay out of it.
87
88
 
88
89
  ## Reseller billing (#3059)
89
90
 
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { f as normalizeFeatureGrants, g as stringifyJson, h as parseJsonObject, m as parseJsonArray, r as extractPlanTerms } from "./plan-terms-BoeXvDhs.js";
3
- import { SmrtCollection, SmrtObject, smrt } from "@happyvertical/smrt-core";
3
+ import { SmrtCollection, SmrtObject, field, smrt } from "@happyvertical/smrt-core";
4
4
  import { TenantScoped, getTenantId, tenantId } from "@happyvertical/smrt-tenancy";
5
5
  //#region src/models/SubscriptionPlan.ts
6
6
  var __defProp = Object.defineProperty;
@@ -28,11 +28,6 @@ var SubscriptionPlan = class extends SmrtObject {
28
28
  description = "";
29
29
  status = "active";
30
30
  sortOrder = 0;
31
- /**
32
- * Plan price in **integer minor units** of {@link currency} — `$19.99` is
33
- * `1999`. The `= 0` initializer is load-bearing: SMRT maps an integer literal
34
- * to INTEGER and a decimal literal to DECIMAL, and money is exact (#2401).
35
- */
36
31
  priceAmount = 0;
37
32
  currency = "USD";
38
33
  billingInterval = "month";
@@ -102,7 +97,7 @@ var SubscriptionPlan = class extends SmrtObject {
102
97
  const effectiveFrom = requestedEffectiveFrom.get(this) ?? /* @__PURE__ */ new Date();
103
98
  requestedEffectiveFrom.delete(this);
104
99
  await super.save(options);
105
- const { publishPlanVersion } = await import("./plan-versioning-Bp0NqTJf.js").then((n) => n.n);
100
+ const { publishPlanVersion } = await import("./plan-versioning-ChDyHPvn.js").then((n) => n.n);
106
101
  const prior = priorTerms.get(this);
107
102
  priorTerms.delete(this);
108
103
  publishedVersions.set(this, await publishPlanVersion(this, effectiveFrom, prior));
@@ -127,7 +122,7 @@ var SubscriptionPlan = class extends SmrtObject {
127
122
  /** The version of this plan in force at `at` (default now). */
128
123
  async getVersionInForce(at = /* @__PURE__ */ new Date()) {
129
124
  if (!this.id) return null;
130
- const { SubscriptionPlanVersionCollection } = await import("./SubscriptionPlanVersionCollection-CV0QPmp0.js").then((n) => n.n);
125
+ const { SubscriptionPlanVersionCollection } = await import("./SubscriptionPlanVersionCollection-yTE_Y-2z.js").then((n) => n.n);
131
126
  return (await SubscriptionPlanVersionCollection.create({ db: this.db })).findInForce(String(this.id), at);
132
127
  }
133
128
  getFeatureGrants() {
@@ -175,6 +170,17 @@ var SubscriptionPlan = class extends SmrtObject {
175
170
  };
176
171
  __publicField(SubscriptionPlan, "__smrtPackage__", "@happyvertical/smrt-subscriptions");
177
172
  __decorateClass([tenantId({ nullable: true })], SubscriptionPlan.prototype, "tenantId", 2);
173
+ __decorateClass([field({ description: "A short code that identifies the plan, such as starter or pro." })], SubscriptionPlan.prototype, "planKey", 2);
174
+ __decorateClass([field({ description: "What the plan is called, as customers see it." })], SubscriptionPlan.prototype, "name", 2);
175
+ __decorateClass([field({ description: "A sentence or two about what the plan offers." })], SubscriptionPlan.prototype, "description", 2);
176
+ __decorateClass([field({ description: "Whether the plan is a draft, on sale (active) or archived." })], SubscriptionPlan.prototype, "status", 2);
177
+ __decorateClass([field({ description: "Where the plan sits when plans are listed; lower numbers come first." })], SubscriptionPlan.prototype, "sortOrder", 2);
178
+ __decorateClass([field({ description: "What the plan costs each billing period, in cents, as a whole number." })], SubscriptionPlan.prototype, "priceAmount", 2);
179
+ __decorateClass([field({ description: "The currency the price is in, such as USD." })], SubscriptionPlan.prototype, "currency", 2);
180
+ __decorateClass([field({ description: "How often the subscriber is billed: every day, week, month or year." })], SubscriptionPlan.prototype, "billingInterval", 2);
181
+ __decorateClass([field({ description: "Whether the plan is free, has a fixed price, or lets the subscriber choose what to pay. It cannot change once the plan is saved." })], SubscriptionPlan.prototype, "pricingMode", 2);
182
+ __decorateClass([field({ description: "For a pay-what-you-can plan, the least a subscriber may pay, in cents." })], SubscriptionPlan.prototype, "minimumAmount", 2);
183
+ __decorateClass([field({ description: "For a pay-what-you-can plan, the amount suggested to the subscriber, in cents." })], SubscriptionPlan.prototype, "suggestedAmount", 2);
178
184
  SubscriptionPlan = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
179
185
  tableName: "_smrt_subscription_plans",
180
186
  api: { include: [
@@ -226,4 +232,4 @@ var SubscriptionPlanCollection = class extends SmrtCollection {
226
232
  //#endregion
227
233
  export { SubscriptionPlanCollection_exports as n, SubscriptionPlan as r, SubscriptionPlanCollection as t };
228
234
 
229
- //# sourceMappingURL=SubscriptionPlanCollection-CqlYYtPg.js.map
235
+ //# sourceMappingURL=SubscriptionPlanCollection-Dmkorx_Q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SubscriptionPlanCollection-Dmkorx_Q.js","names":[],"sources":["../../src/models/SubscriptionPlan.ts","../../src/collections/SubscriptionPlanCollection.ts"],"sourcesContent":["import {\n field,\n SmrtObject,\n type SmrtSaveOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport {\n getTenantId,\n TenantScoped,\n tenantId,\n} from '@happyvertical/smrt-tenancy';\nimport { extractPlanTerms } from '../plan-terms.js';\nimport type {\n BillingInterval,\n JsonObject,\n PlanFeatureGrant,\n PlanTerms,\n PlanThreshold,\n PricingMode,\n SubscriptionPlanOptions,\n SubscriptionPlanStatus,\n} from '../types.js';\nimport {\n normalizeFeatureGrants,\n parseJsonArray,\n parseJsonObject,\n stringifyJson,\n} from '../utils.js';\nimport type { SubscriptionPlanVersion } from './SubscriptionPlanVersion.js';\n\n/** Effective date requested by {@link SubscriptionPlan.publishTerms}. */\nconst requestedEffectiveFrom = new WeakMap<SubscriptionPlan, Date>();\n/** The persisted row a save is about to overwrite, read during validation. */\nconst priorTerms = new WeakMap<SubscriptionPlan, SubscriptionPlan>();\n/** The latest version after the most recent save. */\nconst publishedVersions = new WeakMap<\n SubscriptionPlan,\n SubscriptionPlanVersion\n>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: '_smrt_subscription_plans',\n api: { include: ['list', 'get', 'create', 'update'] },\n cli: { skipApiCheck: true },\n mcp: { include: ['list', 'get'] },\n conflictColumns: ['tenant_id', 'plan_key'],\n})\nexport class SubscriptionPlan extends SmrtObject implements PlanTerms { static __smrtPackage__ = \"@happyvertical/smrt-subscriptions\";\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n @field({\n description:\n 'A short code that identifies the plan, such as starter or pro.',\n })\n planKey: string = '';\n @field({ description: 'What the plan is called, as customers see it.' })\n name: string = '';\n @field({ description: 'A sentence or two about what the plan offers.' })\n description: string = '';\n @field({\n description: 'Whether the plan is a draft, on sale (active) or archived.',\n })\n status: SubscriptionPlanStatus = 'active';\n @field({\n description:\n 'Where the plan sits when plans are listed; lower numbers come first.',\n })\n sortOrder: number = 0;\n /**\n * Plan price in **integer minor units** of {@link currency} — `$19.99` is\n * `1999`. The `= 0` initializer is load-bearing: SMRT maps an integer literal\n * to INTEGER and a decimal literal to DECIMAL, and money is exact (#2401).\n */\n @field({\n description:\n 'What the plan costs each billing period, in cents, as a whole number.',\n })\n priceAmount: number = 0;\n @field({ description: 'The currency the price is in, such as USD.' })\n currency: string = 'USD';\n @field({\n description:\n 'How often the subscriber is billed: every day, week, month or year.',\n })\n billingInterval: BillingInterval = 'month';\n @field({\n description:\n 'Whether the plan is free, has a fixed price, or lets the subscriber choose what to pay. It cannot change once the plan is saved.',\n })\n pricingMode: PricingMode = 'fixed';\n @field({\n description:\n 'For a pay-what-you-can plan, the least a subscriber may pay, in cents.',\n })\n minimumAmount: number = 0;\n @field({\n description:\n 'For a pay-what-you-can plan, the amount suggested to the subscriber, in cents.',\n })\n suggestedAmount: number = 0;\n /** Optional provider binding; plan semantics never require a provider. */\n externalProvider: string | null = null;\n providerProductId: string | null = null;\n providerPriceId: string | null = null;\n /** @deprecated Use providerProductId. */\n stripeProductId: string | null = null;\n /** @deprecated Use providerPriceId. */\n stripePriceId: string | null = null;\n features: string = '[]';\n thresholds: string = '[]';\n metadata: string = '{}';\n\n constructor(options: SubscriptionPlanOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.planKey !== undefined) this.planKey = options.planKey;\n if (options.name !== undefined) this.name = options.name;\n if (options.description !== undefined)\n this.description = options.description;\n if (options.status !== undefined) this.status = options.status;\n if (options.sortOrder !== undefined) this.sortOrder = options.sortOrder;\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 }\n if (options.externalProvider !== undefined) {\n this.externalProvider = options.externalProvider;\n }\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.providerProductId !== undefined) {\n this.providerProductId = options.providerProductId;\n }\n if (options.providerPriceId !== undefined) {\n this.providerPriceId = options.providerPriceId;\n }\n if (options.stripeProductId !== undefined) {\n this.stripeProductId = options.stripeProductId;\n }\n if (options.stripePriceId !== undefined) {\n this.stripePriceId = options.stripePriceId;\n }\n if (options.features !== undefined) this.features = options.features;\n if (options.thresholds !== undefined) this.thresholds = options.thresholds;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n isActive(): boolean {\n return this.status === 'active';\n }\n\n /** The versioned terms currently on this row (the latest published). */\n getTerms(): PlanTerms {\n return extractPlanTerms(this);\n }\n\n /**\n * Persist this plan. When its terms (status, price, pricing configuration,\n * feature grants or thresholds) differ from the latest published version —\n * or no version exists yet — the save then publishes the next immutable\n * {@link SubscriptionPlanVersion}, effective now (#4032). Use\n * {@link publishTerms} to choose a different effective date. Subscribers\n * keep the version pinned for their current period; the new terms apply\n * from their next renewal.\n *\n * A plan saved before versioning existed first records its pre-edit terms\n * as version 1, effective from its creation, so subscriptions sold under\n * them are not re-priced.\n *\n * The row is written before the version. If publishing fails the save\n * rejects with the row already updated; saving again publishes the\n * missing version. Wrap the call in `plan.withTransaction()` when the\n * adapter supports transactions and the two writes must be atomic.\n */\n override async save(options?: SmrtSaveOptions): Promise<this> {\n const effectiveFrom = requestedEffectiveFrom.get(this) ?? new Date();\n requestedEffectiveFrom.delete(this);\n await super.save(options);\n const { publishPlanVersion } = await import('../plan-versioning.js');\n const prior = priorTerms.get(this);\n priorTerms.delete(this);\n publishedVersions.set(\n this,\n await publishPlanVersion(this, effectiveFrom, prior),\n );\n return this;\n }\n\n /**\n * Save this plan and publish its current terms as a version effective at\n * `effectiveFrom` (default now). A future date schedules an amendment: the\n * plan row shows the new terms immediately, but subscriptions starting or\n * renewing before that date pin the earlier version. Versions are\n * monotonic, so `effectiveFrom` may not precede the latest version's.\n *\n * @returns the latest version, which carries the saved terms.\n */\n async publishTerms(\n options: { effectiveFrom?: Date } = {},\n ): Promise<SubscriptionPlanVersion> {\n if (options.effectiveFrom) {\n requestedEffectiveFrom.set(this, options.effectiveFrom);\n }\n await this.save();\n const version = publishedVersions.get(this);\n if (!version) throw new Error('Plan version was not published');\n return version;\n }\n\n /** The version of this plan in force at `at` (default now). */\n async getVersionInForce(\n at = new Date(),\n ): Promise<SubscriptionPlanVersion | null> {\n if (!this.id) return null;\n const { SubscriptionPlanVersionCollection } = await import(\n '../collections/SubscriptionPlanVersionCollection.js'\n );\n const versions = await SubscriptionPlanVersionCollection.create({\n db: this.db,\n });\n return versions.findInForce(String(this.id), at);\n }\n\n getFeatureGrants(): PlanFeatureGrant[] {\n return normalizeFeatureGrants(\n parseJsonArray<string | PlanFeatureGrant>(this.features),\n );\n }\n\n setFeatureGrants(grants: Array<string | PlanFeatureGrant>): void {\n this.features = stringifyJson(normalizeFeatureGrants(grants));\n }\n\n getFeatureKeys(): string[] {\n return this.getFeatureGrants()\n .filter((grant) => grant.enabled !== false)\n .map((grant) => grant.featureKey);\n }\n\n getThresholds(): PlanThreshold[] {\n return parseJsonArray<PlanThreshold>(this.thresholds).filter(\n (threshold) => threshold.metricKey,\n );\n }\n\n setThresholds(thresholds: PlanThreshold[]): void {\n this.thresholds = stringifyJson(thresholds);\n }\n\n getMetadata(): JsonObject {\n return parseJsonObject(this.metadata, {});\n }\n\n setMetadata(metadata: JsonObject): void {\n this.metadata = stringifyJson(metadata);\n }\n\n protected async validateBeforeSave(): Promise<void> {\n await super.validateBeforeSave();\n const { SubscriptionPlanCollection } = await import(\n '../collections/SubscriptionPlanCollection.js'\n );\n const plans = await SubscriptionPlanCollection.create({ db: this.db });\n const persisted =\n (this.id\n ? await plans.get({ id: String(this.id) }, { cache: false })\n : null) ??\n (await plans.findByPlanKey(\n this.tenantId ?? getTenantId() ?? null,\n this.planKey,\n ));\n // A concurrently created natural-key row must never be adopted after this\n // validation: its pricing terms may differ from the terms checked here.\n if (!persisted) this.requireInsertOnSave();\n else priorTerms.set(this, persisted);\n if (\n persisted &&\n (persisted.pricingMode !== this.pricingMode ||\n persisted.minimumAmount !== this.minimumAmount)\n ) {\n throw new Error(\n 'Create a new plan to change pricing mode or minimum after persistence',\n );\n }\n if (!['free', 'fixed', 'pay_what_you_can'].includes(this.pricingMode)) {\n throw new Error('Invalid pricingMode');\n }\n if (this.pricingMode !== 'fixed' && this.priceAmount !== 0) {\n throw new Error(\n 'priceAmount must be zero for free and pay_what_you_can plans',\n );\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 if (\n this.pricingMode === 'pay_what_you_can' &&\n this.suggestedAmount > 0 &&\n this.suggestedAmount < this.minimumAmount\n ) {\n throw new Error('suggestedAmount must be at least minimumAmount');\n }\n }\n}\n\nexport default SubscriptionPlan;\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { SubscriptionPlan } from '../models/SubscriptionPlan.js';\n\nexport class SubscriptionPlanCollection extends SmrtCollection<SubscriptionPlan> {\n static readonly _itemClass = SubscriptionPlan;\n\n async findByPlanKey(\n tenantId: string | null,\n planKey: string,\n ): Promise<SubscriptionPlan | null> {\n const plans = await this.list({ where: { tenantId, planKey }, limit: 1 });\n return plans[0] ?? null;\n }\n\n async findActive(): Promise<SubscriptionPlan[]> {\n return this.list({\n where: { status: 'active' },\n orderBy: 'sortOrder ASC',\n });\n }\n\n async findByProviderPriceId(\n providerPriceId: string,\n ): Promise<SubscriptionPlan | null> {\n const plans = await this.list({ where: { providerPriceId }, limit: 1 });\n if (plans[0]) return plans[0];\n const legacy = await this.list({\n where: { stripePriceId: providerPriceId },\n });\n return (\n legacy.find(\n (plan) => !plan.externalProvider || plan.externalProvider === 'stripe',\n ) ?? null\n );\n }\n\n /** @deprecated Provider-neutral integrations should use findByProviderPriceId. */\n async findByStripePriceId(\n stripePriceId: string,\n ): Promise<SubscriptionPlan | null> {\n const plans = await this.list({ where: { stripePriceId }, limit: 1 });\n return plans[0] ?? null;\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { f as normalizeFeatureGrants, m as parseJsonArray, n as enabledFeatureKeys, o as validThresholds, r as extractPlanTerms } from "./plan-terms-BoeXvDhs.js";
3
- import { SmrtCollection, SmrtObject, foreignKey, smrt } from "@happyvertical/smrt-core";
3
+ import { SmrtCollection, SmrtObject, field, foreignKey, smrt } from "@happyvertical/smrt-core";
4
4
  import { TenantScoped, getTenantId, isSystemContext, tenantId, withSystemContext } from "@happyvertical/smrt-tenancy";
5
5
  //#region src/models/SubscriptionPlanVersion.ts
6
6
  var __defProp = Object.defineProperty;
@@ -26,23 +26,16 @@ var PRICING_MODES = [
26
26
  var SubscriptionPlanVersion = class extends SmrtObject {
27
27
  tenantId = null;
28
28
  planId = "";
29
- /** The plan's natural key, copied for auditing and resolution output. */
30
29
  planKey = "";
31
- /** 1-based sequence within the plan; contiguous and monotonic. */
32
30
  version = 0;
33
- /** Start of this version's effective interval. */
34
31
  effectiveFrom = /* @__PURE__ */ new Date();
35
- /** Plan name when this version was published (display only). */
36
32
  name = "";
37
33
  status = "active";
38
- /** Integer minor units of {@link currency}. */
39
34
  priceAmount = 0;
40
35
  currency = "USD";
41
36
  billingInterval = "month";
42
37
  pricingMode = "fixed";
43
- /** Integer minor units. */
44
38
  minimumAmount = 0;
45
- /** Integer minor units. */
46
39
  suggestedAmount = 0;
47
40
  features = "[]";
48
41
  thresholds = "[]";
@@ -95,7 +88,7 @@ var SubscriptionPlanVersion = class extends SmrtObject {
95
88
  ["minimumAmount", this.minimumAmount],
96
89
  ["suggestedAmount", this.suggestedAmount]
97
90
  ]) if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${name} must be a non-negative integer minor-unit amount`);
98
- const { SubscriptionPlanCollection } = await import("./SubscriptionPlanCollection-CqlYYtPg.js").then((n) => n.n);
91
+ const { SubscriptionPlanCollection } = await import("./SubscriptionPlanCollection-Dmkorx_Q.js").then((n) => n.n);
99
92
  const { SubscriptionPlanVersionCollection } = await Promise.resolve().then(() => SubscriptionPlanVersionCollection_exports);
100
93
  const owner = this.tenantId ?? null;
101
94
  const [plan, latest] = await withSystemContext(async () => {
@@ -114,7 +107,18 @@ var SubscriptionPlanVersion = class extends SmrtObject {
114
107
  };
115
108
  __publicField(SubscriptionPlanVersion, "__smrtPackage__", "@happyvertical/smrt-subscriptions");
116
109
  __decorateClass([tenantId({ nullable: true })], SubscriptionPlanVersion.prototype, "tenantId", 2);
117
- __decorateClass([foreignKey("SubscriptionPlan")], SubscriptionPlanVersion.prototype, "planId", 2);
110
+ __decorateClass([field({ description: "The plan this version belongs to." }), foreignKey("SubscriptionPlan")], SubscriptionPlanVersion.prototype, "planId", 2);
111
+ __decorateClass([field({ description: "The plan code at the time this version was published." })], SubscriptionPlanVersion.prototype, "planKey", 2);
112
+ __decorateClass([field({ description: "Which version of the plan this is, counting from 1." })], SubscriptionPlanVersion.prototype, "version", 2);
113
+ __decorateClass([field({ description: "The date and time from which these terms apply." })], SubscriptionPlanVersion.prototype, "effectiveFrom", 2);
114
+ __decorateClass([field({ description: "What the plan was called when this version was published." })], SubscriptionPlanVersion.prototype, "name", 2);
115
+ __decorateClass([field({ description: "Whether the plan was a draft, on sale (active) or archived under this version." })], SubscriptionPlanVersion.prototype, "status", 2);
116
+ __decorateClass([field({ description: "What the plan cost each billing period under this version, in cents." })], SubscriptionPlanVersion.prototype, "priceAmount", 2);
117
+ __decorateClass([field({ description: "The currency the price is in, such as USD." })], SubscriptionPlanVersion.prototype, "currency", 2);
118
+ __decorateClass([field({ description: "How often the subscriber was billed: every day, week, month or year." })], SubscriptionPlanVersion.prototype, "billingInterval", 2);
119
+ __decorateClass([field({ description: "Whether the plan was free, had a fixed price, or let the subscriber choose what to pay." })], SubscriptionPlanVersion.prototype, "pricingMode", 2);
120
+ __decorateClass([field({ description: "For a pay-what-you-can plan, the least a subscriber could pay, in cents." })], SubscriptionPlanVersion.prototype, "minimumAmount", 2);
121
+ __decorateClass([field({ description: "For a pay-what-you-can plan, the amount suggested to the subscriber, in cents." })], SubscriptionPlanVersion.prototype, "suggestedAmount", 2);
118
122
  SubscriptionPlanVersion = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
119
123
  tableName: "_smrt_subscription_plan_versions",
120
124
  api: { include: ["list", "get"] },
@@ -190,4 +194,4 @@ var SubscriptionPlanVersionCollection = class extends SmrtCollection {
190
194
  //#endregion
191
195
  export { SubscriptionPlanVersionCollection_exports as n, SubscriptionPlanVersion as r, SubscriptionPlanVersionCollection as t };
192
196
 
193
- //# sourceMappingURL=SubscriptionPlanVersionCollection-CV0QPmp0.js.map
197
+ //# sourceMappingURL=SubscriptionPlanVersionCollection-yTE_Y-2z.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SubscriptionPlanVersionCollection-yTE_Y-2z.js","names":[],"sources":["../../src/models/SubscriptionPlanVersion.ts","../../src/collections/SubscriptionPlanVersionCollection.ts"],"sourcesContent":["import { field, 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 @field({ description: 'The plan this version belongs to.' })\n @foreignKey('SubscriptionPlan')\n planId: string = '';\n\n /** The plan's natural key, copied for auditing and resolution output. */\n @field({\n description: 'The plan code at the time this version was published.',\n })\n planKey: string = '';\n /** 1-based sequence within the plan; contiguous and monotonic. */\n @field({\n description: 'Which version of the plan this is, counting from 1.',\n })\n version: number = 0;\n /** Start of this version's effective interval. */\n @field({\n description: 'The date and time from which these terms apply.',\n })\n effectiveFrom: Date = new Date();\n /** Plan name when this version was published (display only). */\n @field({\n description: 'What the plan was called when this version was published.',\n })\n name: string = '';\n @field({\n description:\n 'Whether the plan was a draft, on sale (active) or archived under this version.',\n })\n status: SubscriptionPlanStatus = 'active';\n /** Integer minor units of {@link currency}. */\n @field({\n description:\n 'What the plan cost each billing period under this version, in cents.',\n })\n priceAmount: number = 0;\n @field({ description: 'The currency the price is in, such as USD.' })\n currency: string = 'USD';\n @field({\n description:\n 'How often the subscriber was billed: every day, week, month or year.',\n })\n billingInterval: BillingInterval = 'month';\n @field({\n description:\n 'Whether the plan was free, had a fixed price, or let the subscriber choose what to pay.',\n })\n pricingMode: PricingMode = 'fixed';\n /** Integer minor units. */\n @field({\n description:\n 'For a pay-what-you-can plan, the least a subscriber could pay, in cents.',\n })\n minimumAmount: number = 0;\n /** Integer minor units. */\n @field({\n description:\n 'For a pay-what-you-can plan, the amount suggested to the subscriber, in cents.',\n })\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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"}
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
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";
3
+ import { t as SubscriptionPlanCollection } from "./SubscriptionPlanCollection-Dmkorx_Q.js";
4
+ import { r as SubscriptionPlanVersion, t as SubscriptionPlanVersionCollection } from "./SubscriptionPlanVersionCollection-yTE_Y-2z.js";
5
5
  import { withSystemContext } from "@happyvertical/smrt-tenancy";
6
6
  //#region src/plan-versioning.ts
7
7
  var plan_versioning_exports = /* @__PURE__ */ __exportAll({
@@ -139,4 +139,4 @@ function periodBoundary(subscription) {
139
139
  //#endregion
140
140
  export { plan_versioning_exports as n, snapshotLegacyPlanVersion as r, pinSubscriptionPlanVersion as t };
141
141
 
142
- //# sourceMappingURL=plan-versioning-Bp0NqTJf.js.map
142
+ //# sourceMappingURL=plan-versioning-ChDyHPvn.js.map
@@ -1 +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":";;;;;;;;;;;;AA+BA,eAAsB,0BACpB,MACkB;CAClB,IAAI,CAAC,KAAK,IAAI,OAAO;CACrB,MAAM,WAAW,MAAM,kCAAkC,OAAO,EAC9D,IAAI,KAAK,GACX,CAAC;CACD,MAAM,QAAQ,KAAK,YAAY;CAI/B,IAAI,MAHiB,wBACnB,SAAS,WAAW,OAAO,KAAK,EAAE,GAAG,KAAK,CAC5C,GACY,OAAO;CACnB,MAAM,cAAc,MAAM,MAAM,GAAG,mBAAmB,IAAI,CAAC;CAC3D,OAAO;AACT;AAQA,eAAsB,mBACpB,MACA,eACA,OACkC;CAClC,MAAM,WAAW,MAAM,kCAAkC,OAAO,EAC9D,IAAI,KAAK,GACX,CAAC;CACD,MAAM,QAAQ,KAAK,YAAY;CAC/B,IAAI,SAAS,MAAM,wBACjB,SAAS,WAAW,OAAO,KAAK,EAAE,GAAG,KAAK,CAC5C;CACA,IAAI,CAAC,UAAU,SAAS,OAAO,MAAM,EAAE,MAAM,OAAO,KAAK,EAAE,GACzD,SAAS,MAAM,cAAc,MAAM,OAAO,GAAG,mBAAmB,KAAK,CAAC;CAExE,IAAI,UAAU,cAAc,QAAQ,IAAI,GAAG,OAAO;CAClD,OAAO,cAAc,MAAM,OAAO,QAAQ,WAAW,KAAK,GAAG,aAAa;AAC5E;AAEA,eAAe,cACb,MACA,OACA,SACA,eACkC;CAClC,MAAM,SAAS,IAAI,wBAAwB;EACzC,IAAI,KAAK;EACT,UAAU,KAAK,YAAY;EAC3B,QAAQ,OAAO,KAAK,EAAE;EACtB,SAAS,KAAK;EACd,MAAM,KAAK;EACX;EACA;EACA,GAAG,iBAAiB,KAAK;CAC3B,CAAC;CACD,MAAM,OAAO,WAAW;CACxB,MAAM,OAAO,KAAK;CAClB,OAAO;AACT;AAEA,SAAS,mBAAmB,MAA8B;CACxD,MAAM,UAAU,KAAK,aAAa,IAAI,KAAK,KAAK,UAAU,IAAI;CAC9D,OAAO,WAAW,CAAC,OAAO,MAAM,QAAQ,QAAQ,CAAC,IAAI,0BAAU,IAAI,KAAK,CAAC;AAC3E;AAqBA,eAAsB,2BACpB,cACA,WACA,aACA,sBAAM,IAAI,KAAK,GACA;CACf,MAAM,WAAW,MAAM,kCAAkC,OAAO,EAC9D,IAAI,aAAa,GACnB,CAAC;CACD,MAAM,WAAW,eAAe,YAAY;CAC5C,MAAM,YACJ,aAAa,iBACb,aAAa,mBAAmB,WAAW,iBAAiB,QACxD,aAAa,gBACb,aAAa;CACnB,MAAM,mBACJ,aAAa,WAAW,eAAe,YAAY,QAAQ,KAAK,IAAI;CACtE,IAAI,aAAa,WAAW,gBAAgB,CAAC,kBAC3C,MAAM,IAAI,MAAM,uDAAuD;CAEzE,MAAM,OACJ,SACA,QACA,OAAO,OACJ;EACH,aAAa,gBAAgB,SAAS,KAAK,OAAO,QAAQ,EAAE,IAAI;EAChE,aAAa,sBAAsB,UAAU,MAAM;EACnD,aAAa,uBAAuB,UAAU,SAAS;EACvD,aAAa,qBAAqB,UAAU,OAAO;CACrD;CACA,MAAM,OAAO,OAAO,OAAe;EACjC,MAAM,UAAU,MAAM,YAAY,UAAU,EAAE;EAC9C,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,6BAA6B;EAC3D,IAAI,QAAQ,WAAW,aAAa,QAClC,MAAM,IAAI,MAAM,uDAAuD;EAEzE,OAAO;CACT;CAEA,MAAM,aAAa,OAAO,WAAiC;EACzD,MAAM,UAAU,MAAM,SAAS,YAAY,aAAa,QAAQ,QAAQ;EACxE,IAAI,aAAa,cAAc,SAAS,IAAI;GAC1C,MAAM,SAAS,MAAM,KAAK,SAAS;GACnC,IAAI,CAAC,kBACH,MAAM,IAAI,MACR,2IAEF;GAEF,IAAI,QAAQ,cAAc,gBAAgB;GAC1C;EACF;EACA,IAAI,oBAAoB,SAAS;GAC/B,IAAI,SAAS,cAAc,gBAAgB;GAC3C;EACF;EACA,IAAI,SAAS,MAAM;CACrB;CAEA,IAAI,CAAC,WAAW;EACd,MAAM,WAAW,cAAc;EAC/B;CACF;CACA,IAAI,UAAU,WAAW,QAAQ,GAAG;EAClC,MAAM,WAAW,SAAS;EAI1B,IACE,SAAS,QAAQ,IAAI,IAAI,QAAQ,KACjC,CAAC,oBACD,UAAU,WAAW,GAAG,MACvB,aAAa,kBAAkB,UAAU,iBACxC,aAAa,WAAW,UAAU,SACpC;GACA,MAAMA,UAAS,MAAM,YACnB,UACA,WACA,eAAe,SAAS,CAC1B;GACA,MAAMC,UAA2B,aAAa,gBAC1C,MAAM,YAAY,UAAU,aAAa,aAAa,IACtD,OACE,MAAM,2BAA2B,OAAO,EAAE,IAAI,aAAa,GAAG,CAAC,EAAA,CAC/D,IAAI,EAAE,IAAI,aAAa,OAAO,GAAG,EAAE,OAAO,MAAM,CAAC;GACvD,IAAID,YAAW,CAACC,WAAU,CAAC,qBAAqBD,SAAQC,OAAM,IAC5D,MAAM,IAAI,MACR,iIAEF;EAEJ;EACA;CACF;CAEA,IAAI,EADgB,UAAU,WAAW,aAAa,WAClC,CAAC,aAAa,CAAC,aAAa;EAC9C,IAAI,CAAC,UAAU,eAAe;GAE5B,IACE,MAAM,SAAS,YAAY,aAAa,QAAQ,QAAQ,GACxD,WACF;GACA;EACF;EAEA,aAAa,gBAAgB,UAAU;EACvC,aAAa,sBAAsB,UAAU;EAC7C,aAAa,uBAAuB,UAAU;EAC9C,aAAa,qBAAqB,UAAU;EAC5C;CACF;CAIA,MAAM,OAAO,YACT,MAAM,KAAK,SAAS,IACpB,MAAM,SAAS,YAAY,aAAa,QAAQ,GAAG;CACvD,IAAI,kBAAkB;EACpB,IAAI,MAAM,cAAc,gBAAgB;EACxC;CACF;CACA,IAAI,CAAC,UAAU,WAAW,GAAG,GAAG;EAC9B,IAAI,MAAM,cAAc;EACxB;CACF;CACA,MAAM,SACJ,QACC,OACC,MAAM,2BAA2B,OAAO,EAAE,IAAI,aAAa,GAAG,CAAC,EAAA,CAC/D,IAAI,EAAE,IAAI,aAAa,OAAO,GAAG,EAAE,OAAO,MAAM,CAAC;CACrD,MAAM,SAAS,MAAM,YAAY,UAAU,WAAW,QAAQ;CAC9D,IAAI,WAAW,CAAC,UAAU,CAAC,qBAAqB,QAAQ,MAAM,IAC5D,MAAM,IAAI,MACR,kNAGF;CAEF,IAAI,MAAM,WAAW,aAAa,QAAQ,KAAK,KAAK,EAAE;AACxD;AAMA,SAAS,UAAU,WAA+B,UAAyB;CACzE,MAAM,WAAW,eAAe,SAAS,CAAA,CAAE,QAAQ;CACnD,IAAI,SAAS,QAAQ,MAAM,UAAU,OAAO;CAC5C,MAAM,MAAM,UAAU,mBAClB,IAAI,KAAK,UAAU,gBAAgB,CAAA,CAAE,QAAQ,IAC7C;CACJ,OAAO,QAAQ,OACX,SAAS,QAAQ,IAAI,WACrB,SAAS,QAAQ,KAAK;AAC5B;AAGA,eAAe,YACb,UACA,WACA,UAC2B;CAC3B,IAAI,UAAU,eAAe;EAC3B,MAAM,SAAS,MAAM,YAAY,UAAU,UAAU,aAAa;EAClE,IAAI,QAAQ,OAAO;CACrB;CACA,MAAM,UAAU,MAAM,SAAS,YAAY,UAAU,QAAQ,QAAQ;CACrE,IAAI,SAAS,OAAO;CAIpB,QAAO,MAHa,2BAA2B,OAAO,EACpD,IAAI,UAAU,GAChB,CAAC,EAAA,CACY,IAAI,EAAE,IAAI,UAAU,OAAO,GAAG,EAAE,OAAO,MAAM,CAAC;AAC7D;AAEA,eAAe,YACb,UACA,IACyC;CACzC,OAAO,SAAS,gBAAgB,EAAE;AACpC;AAGO,SAAS,eACd,cACM;CACN,OAAO,IAAI,KAAK,aAAa,sBAAsB,aAAa,SAAS;AAC3E"}
1
+ {"version":3,"file":"plan-versioning-ChDyHPvn.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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"}
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export type { PriceBookKind, PricingStrategy, SpendingBasis, SpendingPeriod, Spe
14
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
15
  export { enabledFeatureKeys, extractPlanTerms, isEntitlementUpgrade, PLAN_TERM_FIELDS, samePlanTerms, validThresholds, } from './plan-terms.js';
16
16
  export type { PlanVersionReplacement } from './plan-versioning.js';
17
+ export { SubscriptionPlansRecipe } from './recipes.js';
17
18
  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';
18
19
  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
20
  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';
@@ -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,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"}
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;AAEnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,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"}