@happyvertical/smrt-subscriptions 0.57.36 → 0.57.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-subscriptions",
3
- "version": "0.57.36",
3
+ "version": "0.57.38",
4
4
  "description": "Tenant subscriptions, entitlement resolution, usage thresholds, and subscription UI for SMRT",
5
5
  "type": "module",
6
6
  "smrtRawPrimitives": "strict",
@@ -26,10 +26,10 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@happyvertical/smrt-core": "0.57.36",
30
- "@happyvertical/smrt-tenancy": "0.57.36",
31
- "@happyvertical/smrt-ui": "0.57.36",
32
- "@happyvertical/sql": "^0.109.0"
29
+ "@happyvertical/smrt-core": "0.57.38",
30
+ "@happyvertical/smrt-tenancy": "0.57.38",
31
+ "@happyvertical/smrt-ui": "0.57.38",
32
+ "@happyvertical/sql": "^0.110.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "svelte": "^5.57.0"
@@ -40,7 +40,7 @@
40
40
  }
41
41
  },
42
42
  "devDependencies": {
43
- "@happyvertical/smrt-vitest": "0.57.36",
43
+ "@happyvertical/smrt-vitest": "0.57.38",
44
44
  "@sveltejs/package": "^2.5.8",
45
45
  "@types/node": "24.13.6",
46
46
  "svelte": "^5.57.1",
@@ -1 +0,0 @@
1
- {"version":3,"file":"SubscriptionPlanCollection-CqlYYtPg.js","names":[],"sources":["../../src/models/SubscriptionPlan.ts","../../src/collections/SubscriptionPlanCollection.ts"],"sourcesContent":["import {\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 planKey: string = '';\n name: string = '';\n description: string = '';\n status: SubscriptionPlanStatus = 'active';\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 priceAmount: number = 0;\n currency: string = 'USD';\n billingInterval: BillingInterval = 'month';\n pricingMode: PricingMode = 'fixed';\n minimumAmount: number = 0;\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 +0,0 @@
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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"}