diff --git a/packages/core/src/libraries/quota.ts b/packages/core/src/libraries/quota.ts index d09f8c3ef54..831a848755d 100644 --- a/packages/core/src/libraries/quota.ts +++ b/packages/core/src/libraries/quota.ts @@ -2,6 +2,7 @@ import { ReservedPlanId } from '@logto/schemas'; import { EnvSet } from '#src/env-set/index.js'; import RequestError from '#src/errors/RequestError/index.js'; +import { type SubscriptionLibrary } from '#src/libraries/subscription.js'; import assertThat from '#src/utils/assert-that.js'; import { getTenantSubscriptionData, @@ -28,7 +29,10 @@ const shouldReportSubscriptionUpdates = ( ) => (paidReservedPlans.has(planId) || isEnterprisePlan) && isReportSubscriptionUpdatesUsageKey(key); -export const createQuotaLibrary = (cloudConnection: CloudConnectionLibrary) => { +export const createQuotaLibrary = ( + cloudConnection: CloudConnectionLibrary, + subscription: SubscriptionLibrary +) => { const guardTenantUsageByKey = async (key: keyof SubscriptionUsage) => { const { isCloud, isIntegrationTest } = EnvSet.values; @@ -42,18 +46,15 @@ export const createQuotaLibrary = (cloudConnection: CloudConnectionLibrary) => { return; } - const { - planId, - quota: fullQuota, - usage: fullUsage, - isEnterprisePlan, - } = await getTenantSubscriptionData(cloudConnection); + const { planId, isEnterprisePlan, quota: fullQuota } = await subscription.getSubscriptionData(); // Do not block Pro/Enterprise plan from adding add-on resources. if (shouldReportSubscriptionUpdates(planId, isEnterprisePlan, key)) { return; } + const { usage: fullUsage } = await getTenantSubscriptionData(cloudConnection); + // Type `SubscriptionQuota` and type `SubscriptionUsage` are sharing keys, this design helps us to compare the usage with the quota limit in a easier way. const { [key]: limit } = fullQuota; const { [key]: usage } = fullUsage; @@ -165,7 +166,7 @@ export const createQuotaLibrary = (cloudConnection: CloudConnectionLibrary) => { return; } - const { planId, isEnterprisePlan } = await getTenantSubscriptionData(cloudConnection); + const { planId, isEnterprisePlan } = await subscription.getSubscriptionData(); if (shouldReportSubscriptionUpdates(planId, isEnterprisePlan, key)) { await reportSubscriptionUpdates(cloudConnection, key); diff --git a/packages/core/src/tenants/Libraries.ts b/packages/core/src/tenants/Libraries.ts index 2c855d352f7..32c93d9cc66 100644 --- a/packages/core/src/tenants/Libraries.ts +++ b/packages/core/src/tenants/Libraries.ts @@ -16,6 +16,7 @@ import { createScopeLibrary } from '#src/libraries/scope.js'; import { createSignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js'; import { createSocialLibrary } from '#src/libraries/social.js'; import { createSsoConnectorLibrary } from '#src/libraries/sso-connector.js'; +import { type SubscriptionLibrary } from '#src/libraries/subscription.js'; import { createUserLibrary } from '#src/libraries/user.js'; import { createVerificationStatusLibrary } from '#src/libraries/verification-status.js'; @@ -42,7 +43,7 @@ export default class Libraries { roleScopes = createRoleScopeLibrary(this.queries); domains = createDomainLibrary(this.queries); protectedApps = createProtectedAppLibrary(this.queries); - quota = createQuotaLibrary(this.cloudConnection); + quota = createQuotaLibrary(this.cloudConnection, this.subscription); ssoConnectors = createSsoConnectorLibrary(this.queries); signInExperiences = createSignInExperienceLibrary( this.queries, @@ -64,6 +65,7 @@ export default class Libraries { // Explicitly passing connector library to eliminate dependency issue private readonly connectors: ConnectorLibrary, private readonly cloudConnection: CloudConnectionLibrary, - private readonly logtoConfigs: LogtoConfigLibrary + private readonly logtoConfigs: LogtoConfigLibrary, + private readonly subscription: SubscriptionLibrary ) {} } diff --git a/packages/core/src/tenants/Tenant.ts b/packages/core/src/tenants/Tenant.ts index 09cb65ac1d1..f70ec43d953 100644 --- a/packages/core/src/tenants/Tenant.ts +++ b/packages/core/src/tenants/Tenant.ts @@ -85,15 +85,21 @@ export default class Tenant implements TenantContext { public readonly logtoConfigs = createLogtoConfigLibrary(queries), public readonly cloudConnection = createCloudConnectionLibrary(logtoConfigs), public readonly connectors = createConnectorLibrary(queries, cloudConnection), + public readonly subscription = new SubscriptionLibrary( + id, + queries, + cloudConnection, + redisCache + ), public readonly libraries = new Libraries( id, queries, connectors, cloudConnection, - logtoConfigs + logtoConfigs, + subscription ), - public readonly sentinel = new BasicSentinel(envSet.pool), - public readonly subscription = new SubscriptionLibrary(id, queries, cloudConnection, redisCache) + public readonly sentinel = new BasicSentinel(envSet.pool) ) { const isAdminTenant = id === adminTenantId; const mountedApps = [ diff --git a/packages/core/src/test-utils/tenant.ts b/packages/core/src/test-utils/tenant.ts index 297d58b475a..a17d2b0e425 100644 --- a/packages/core/src/test-utils/tenant.ts +++ b/packages/core/src/test-utils/tenant.ts @@ -87,21 +87,22 @@ export class MockTenant implements TenantContext { ...createConnectorLibrary(this.queries, this.cloudConnection), ...connectorsOverride, }; - this.libraries = new Libraries( + this.sentinel = new MockSentinel(); + this.subscription = new SubscriptionLibrary( this.id, this.queries, - this.connectors, this.cloudConnection, - this.logtoConfigs + new TtlCache(60_000) ); - this.setPartial('libraries', librariesOverride); - this.sentinel = new MockSentinel(); - this.subscription = new SubscriptionLibrary( + this.libraries = new Libraries( this.id, this.queries, + this.connectors, this.cloudConnection, - new TtlCache(60_000) + this.logtoConfigs, + this.subscription ); + this.setPartial('libraries', librariesOverride); } public async invalidateCache() { diff --git a/packages/core/src/utils/subscription/index.ts b/packages/core/src/utils/subscription/index.ts index c167b1119f1..c8b46394821 100644 --- a/packages/core/src/utils/subscription/index.ts +++ b/packages/core/src/utils/subscription/index.ts @@ -30,20 +30,15 @@ export const getTenantSubscription = async ( export const getTenantSubscriptionData = async ( cloudConnection: CloudConnectionLibrary ): Promise<{ - planId: string; - isEnterprisePlan: boolean; quota: SubscriptionQuota; usage: SubscriptionUsage; resources: Record; roles: Record; }> => { const client = await cloudConnection.getClient(); - const [{ planId, isEnterprisePlan }, { quota, usage, resources, roles }] = await Promise.all([ - client.get('/api/tenants/my/subscription'), - client.get('/api/tenants/my/subscription-usage'), - ]); + const { quota, usage, resources, roles } = await client.get('/api/tenants/my/subscription-usage'); - return { planId, isEnterprisePlan, quota, usage, resources, roles }; + return { quota, usage, resources, roles }; }; export const reportSubscriptionUpdates = async (