2025-01-03 15:59:57 +01:00
|
|
|
import { get } from "svelte/store"
|
|
|
|
import { API } from "@/api"
|
|
|
|
import { auth, admin } from "@/stores/portal"
|
|
|
|
import { Constants } from "@budibase/frontend-core"
|
|
|
|
import { StripeStatus } from "@/components/portal/licensing/constants"
|
|
|
|
import {
|
2025-01-03 16:10:39 +01:00
|
|
|
License,
|
2025-01-03 15:59:57 +01:00
|
|
|
MonthlyQuotaName,
|
|
|
|
PlanModel,
|
|
|
|
QuotaUsage,
|
|
|
|
StaticQuotaName,
|
|
|
|
} from "@budibase/types"
|
|
|
|
import { BudiStore } from "../BudiStore"
|
|
|
|
|
|
|
|
const UNLIMITED = -1
|
|
|
|
const ONE_DAY_MILLIS = 86400000
|
|
|
|
|
|
|
|
type MonthlyMetrics = { [key in MonthlyQuotaName]?: number }
|
|
|
|
type StaticMetrics = { [key in StaticQuotaName]?: number }
|
2025-01-03 16:10:39 +01:00
|
|
|
type UsageMetrics = MonthlyMetrics & StaticMetrics
|
2025-01-03 15:59:57 +01:00
|
|
|
|
|
|
|
interface LicensingState {
|
|
|
|
goToUpgradePage: () => void
|
|
|
|
goToPricingPage: () => void
|
|
|
|
// the top level license
|
2025-01-03 16:10:39 +01:00
|
|
|
license?: License
|
2025-01-03 15:59:57 +01:00
|
|
|
isFreePlan: boolean
|
|
|
|
isEnterprisePlan: boolean
|
|
|
|
isBusinessPlan: boolean
|
|
|
|
// features
|
|
|
|
groupsEnabled: boolean
|
|
|
|
backupsEnabled: boolean
|
|
|
|
brandingEnabled: boolean
|
|
|
|
scimEnabled: boolean
|
|
|
|
environmentVariablesEnabled: boolean
|
|
|
|
budibaseAIEnabled: boolean
|
|
|
|
customAIConfigsEnabled: boolean
|
|
|
|
auditLogsEnabled: boolean
|
|
|
|
// the currently used quotas from the db
|
|
|
|
quotaUsage?: QuotaUsage
|
|
|
|
// derived quota metrics for percentages used
|
2025-01-03 16:10:39 +01:00
|
|
|
usageMetrics?: UsageMetrics
|
2025-01-03 15:59:57 +01:00
|
|
|
// quota reset
|
2025-01-03 16:10:39 +01:00
|
|
|
quotaResetDaysRemaining?: number
|
|
|
|
quotaResetDate?: Date
|
2025-01-03 15:59:57 +01:00
|
|
|
// failed payments
|
2025-01-03 16:10:39 +01:00
|
|
|
accountPastDue: boolean
|
|
|
|
pastDueEndDate?: Date
|
|
|
|
pastDueDaysRemaining?: number
|
|
|
|
accountDowngraded: boolean
|
2025-01-03 15:59:57 +01:00
|
|
|
// user limits
|
2025-01-03 16:10:39 +01:00
|
|
|
userCount?: number
|
|
|
|
userLimit?: number
|
2025-01-03 15:59:57 +01:00
|
|
|
userLimitReached: boolean
|
|
|
|
errUserLimit: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
class LicensingStore extends BudiStore<LicensingState> {
|
|
|
|
constructor() {
|
|
|
|
super({
|
|
|
|
// navigation
|
|
|
|
goToUpgradePage: () => {},
|
|
|
|
goToPricingPage: () => {},
|
|
|
|
// the top level license
|
|
|
|
license: undefined,
|
|
|
|
isFreePlan: true,
|
|
|
|
isEnterprisePlan: true,
|
|
|
|
isBusinessPlan: true,
|
|
|
|
// features
|
|
|
|
groupsEnabled: false,
|
|
|
|
backupsEnabled: false,
|
|
|
|
brandingEnabled: false,
|
|
|
|
scimEnabled: false,
|
|
|
|
environmentVariablesEnabled: false,
|
|
|
|
budibaseAIEnabled: false,
|
|
|
|
customAIConfigsEnabled: false,
|
|
|
|
auditLogsEnabled: false,
|
|
|
|
// the currently used quotas from the db
|
|
|
|
quotaUsage: undefined,
|
|
|
|
// derived quota metrics for percentages used
|
|
|
|
usageMetrics: undefined,
|
|
|
|
// quota reset
|
|
|
|
quotaResetDaysRemaining: undefined,
|
|
|
|
quotaResetDate: undefined,
|
|
|
|
// failed payments
|
2025-01-03 16:10:39 +01:00
|
|
|
accountPastDue: false,
|
2025-01-03 15:59:57 +01:00
|
|
|
pastDueEndDate: undefined,
|
|
|
|
pastDueDaysRemaining: undefined,
|
2025-01-03 16:10:39 +01:00
|
|
|
accountDowngraded: false,
|
2025-01-03 15:59:57 +01:00
|
|
|
// user limits
|
|
|
|
userCount: undefined,
|
|
|
|
userLimit: undefined,
|
|
|
|
userLimitReached: false,
|
|
|
|
errUserLimit: false,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2025-01-03 16:10:39 +01:00
|
|
|
usersLimitReached(userCount: number, userLimit = get(this.store).userLimit) {
|
|
|
|
if (userLimit === UNLIMITED || userLimit === undefined) {
|
2025-01-03 15:59:57 +01:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
return userCount >= userLimit
|
|
|
|
}
|
|
|
|
|
2025-01-03 16:10:39 +01:00
|
|
|
usersLimitExceeded(userCount: number, userLimit = get(this.store).userLimit) {
|
|
|
|
if (userLimit === UNLIMITED || userLimit === undefined) {
|
2025-01-03 15:59:57 +01:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
return userCount > userLimit
|
|
|
|
}
|
|
|
|
|
|
|
|
async isCloud() {
|
|
|
|
let adminStore = get(admin)
|
|
|
|
if (!adminStore.loaded) {
|
|
|
|
await admin.init()
|
|
|
|
adminStore = get(admin)
|
|
|
|
}
|
|
|
|
return adminStore.cloud
|
|
|
|
}
|
|
|
|
|
|
|
|
async init() {
|
|
|
|
this.setNavigation()
|
|
|
|
this.setLicense()
|
|
|
|
await this.setQuotaUsage()
|
|
|
|
}
|
|
|
|
|
|
|
|
setNavigation() {
|
|
|
|
const adminStore = get(admin)
|
|
|
|
const authStore = get(auth)
|
|
|
|
|
|
|
|
const upgradeUrl = authStore?.user?.accountPortalAccess
|
|
|
|
? `${adminStore.accountPortalUrl}/portal/upgrade`
|
|
|
|
: "/builder/portal/account/upgrade"
|
|
|
|
|
|
|
|
const goToUpgradePage = () => {
|
|
|
|
window.location.href = upgradeUrl
|
|
|
|
}
|
|
|
|
const goToPricingPage = () => {
|
|
|
|
window.open("https://budibase.com/pricing/", "_blank")
|
|
|
|
}
|
|
|
|
this.update(state => {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
goToUpgradePage,
|
|
|
|
goToPricingPage,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
setLicense() {
|
|
|
|
const license = get(auth).user?.license
|
|
|
|
const planType = license?.plan.type
|
|
|
|
const features = license?.features || []
|
|
|
|
const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
|
|
|
|
const isFreePlan = planType === Constants.PlanType.FREE
|
|
|
|
const isBusinessPlan = planType === Constants.PlanType.BUSINESS
|
|
|
|
const isEnterpriseTrial =
|
|
|
|
planType === Constants.PlanType.ENTERPRISE_BASIC_TRIAL
|
|
|
|
const groupsEnabled = features.includes(Constants.Features.USER_GROUPS)
|
|
|
|
const backupsEnabled = features.includes(Constants.Features.APP_BACKUPS)
|
|
|
|
const scimEnabled = features.includes(Constants.Features.SCIM)
|
|
|
|
const environmentVariablesEnabled = features.includes(
|
|
|
|
Constants.Features.ENVIRONMENT_VARIABLES
|
|
|
|
)
|
|
|
|
const enforceableSSO = features.includes(Constants.Features.ENFORCEABLE_SSO)
|
|
|
|
const brandingEnabled = features.includes(Constants.Features.BRANDING)
|
|
|
|
const auditLogsEnabled = features.includes(Constants.Features.AUDIT_LOGS)
|
|
|
|
const syncAutomationsEnabled = features.includes(
|
|
|
|
Constants.Features.SYNC_AUTOMATIONS
|
|
|
|
)
|
|
|
|
const triggerAutomationRunEnabled = features.includes(
|
|
|
|
Constants.Features.TRIGGER_AUTOMATION_RUN
|
|
|
|
)
|
|
|
|
const perAppBuildersEnabled = features.includes(
|
|
|
|
Constants.Features.APP_BUILDERS
|
|
|
|
)
|
|
|
|
const budibaseAIEnabled = features.includes(Constants.Features.BUDIBASE_AI)
|
|
|
|
const customAIConfigsEnabled = features.includes(
|
|
|
|
Constants.Features.AI_CUSTOM_CONFIGS
|
|
|
|
)
|
|
|
|
this.update(state => {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
license,
|
|
|
|
isEnterprisePlan,
|
|
|
|
isFreePlan,
|
|
|
|
isBusinessPlan,
|
|
|
|
isEnterpriseTrial,
|
|
|
|
groupsEnabled,
|
|
|
|
backupsEnabled,
|
|
|
|
brandingEnabled,
|
|
|
|
budibaseAIEnabled,
|
|
|
|
customAIConfigsEnabled,
|
|
|
|
scimEnabled,
|
|
|
|
environmentVariablesEnabled,
|
|
|
|
auditLogsEnabled,
|
|
|
|
enforceableSSO,
|
|
|
|
syncAutomationsEnabled,
|
|
|
|
triggerAutomationRunEnabled,
|
|
|
|
perAppBuildersEnabled,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async setQuotaUsage() {
|
|
|
|
const quotaUsage = await API.getQuotaUsage()
|
|
|
|
this.update(state => {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
quotaUsage,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
await this.setUsageMetrics()
|
|
|
|
}
|
|
|
|
|
|
|
|
async setUsageMetrics() {
|
|
|
|
const usage = get(this.store).quotaUsage
|
|
|
|
const license = get(auth).user?.license
|
|
|
|
const now = new Date()
|
|
|
|
if (!license || !usage) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Process monthly metrics
|
|
|
|
const monthlyMetrics = [
|
|
|
|
MonthlyQuotaName.QUERIES,
|
|
|
|
MonthlyQuotaName.AUTOMATIONS,
|
|
|
|
].reduce((acc: MonthlyMetrics, key) => {
|
|
|
|
const limit = license.quotas.usage.monthly[key].value
|
2025-01-08 10:26:44 +01:00
|
|
|
const used = ((usage.monthly.current?.[key] || 0) / limit) * 100
|
2025-01-03 15:59:57 +01:00
|
|
|
acc[key] = limit > -1 ? Math.floor(used) : -1
|
|
|
|
return acc
|
|
|
|
}, {})
|
|
|
|
|
|
|
|
// Process static metrics
|
|
|
|
const staticMetrics = [StaticQuotaName.APPS, StaticQuotaName.ROWS].reduce(
|
|
|
|
(acc: StaticMetrics, key) => {
|
|
|
|
const limit = license.quotas.usage.static[key].value
|
2025-01-08 10:26:44 +01:00
|
|
|
const used = ((usage.usageQuota[key] || 0) / limit) * 100
|
2025-01-03 15:59:57 +01:00
|
|
|
acc[key] = limit > -1 ? Math.floor(used) : -1
|
|
|
|
return acc
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
)
|
|
|
|
|
|
|
|
const getDaysBetween = (dateStart: Date, dateEnd: Date) => {
|
|
|
|
return dateEnd > dateStart
|
|
|
|
? Math.round((dateEnd.getTime() - dateStart.getTime()) / ONE_DAY_MILLIS)
|
|
|
|
: 0
|
|
|
|
}
|
|
|
|
|
|
|
|
const quotaResetDate = new Date(usage.quotaReset)
|
|
|
|
const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
|
|
|
|
|
|
|
|
const accountDowngraded =
|
2025-01-03 16:10:39 +01:00
|
|
|
!!license.billing?.subscription?.downgradeAt &&
|
2025-01-03 15:59:57 +01:00
|
|
|
license.billing?.subscription?.downgradeAt <= now.getTime() &&
|
|
|
|
license.billing?.subscription?.status === StripeStatus.PAST_DUE &&
|
|
|
|
license.plan.type === Constants.PlanType.FREE
|
|
|
|
|
|
|
|
const pastDueAtMilliseconds = license.billing?.subscription?.pastDueAt
|
|
|
|
const downgradeAtMilliseconds = license.billing?.subscription?.downgradeAt
|
|
|
|
let pastDueDaysRemaining: number
|
|
|
|
let pastDueEndDate: Date
|
|
|
|
|
|
|
|
if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
|
|
|
|
pastDueEndDate = new Date(downgradeAtMilliseconds)
|
|
|
|
pastDueDaysRemaining = getDaysBetween(
|
|
|
|
new Date(pastDueAtMilliseconds),
|
|
|
|
pastDueEndDate
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const userQuota = license.quotas.usage.static.users
|
|
|
|
const userLimit = userQuota.value
|
|
|
|
const userCount = usage.usageQuota.users
|
|
|
|
const userLimitReached = this.usersLimitReached(userCount, userLimit)
|
|
|
|
const userLimitExceeded = this.usersLimitExceeded(userCount, userLimit)
|
|
|
|
const isCloudAccount = await this.isCloud()
|
|
|
|
const errUserLimit =
|
|
|
|
isCloudAccount &&
|
|
|
|
license.plan.model === PlanModel.PER_USER &&
|
|
|
|
userLimitExceeded
|
|
|
|
|
|
|
|
this.update(state => {
|
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
usageMetrics: { ...monthlyMetrics, ...staticMetrics },
|
|
|
|
quotaResetDaysRemaining,
|
|
|
|
quotaResetDate,
|
|
|
|
accountDowngraded,
|
|
|
|
accountPastDue: pastDueAtMilliseconds != null,
|
|
|
|
pastDueEndDate,
|
|
|
|
pastDueDaysRemaining,
|
|
|
|
// user limits
|
|
|
|
userCount,
|
|
|
|
userLimit,
|
|
|
|
userLimitReached,
|
|
|
|
errUserLimit,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const licensing = new LicensingStore()
|