refactor(api): move license expiry notice flag onto license detail (#40128)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Xiyuan Chen
2026-08-06 20:53:35 -07:00
committed by GitHub
parent 1375dc3864
commit ab0e131e87
12 changed files with 71 additions and 44 deletions
+1 -1
View File
@@ -18937,6 +18937,7 @@ Enum class for large language model mode.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| expired_at | string | | Yes |
| license_expiry_notice_enabled | boolean | | Yes |
| seats | [LicenseLimitationModel](#licenselimitationmodel) | | Yes |
| status | [LicenseStatus](#licensestatus) | | Yes |
| workspaces | [LicenseLimitationModel](#licenselimitationmodel) | | Yes |
@@ -22287,7 +22288,6 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication.
| enable_email_password_login | boolean, <br>**Default:** true | | Yes |
| enable_explore_banner | boolean | | Yes |
| enable_learn_app | boolean, <br>**Default:** true | | Yes |
| enable_license_expiry_notice | boolean, <br>**Default:** true | | Yes |
| enable_marketplace | boolean | | Yes |
| enable_social_oauth_login | boolean | | Yes |
| enable_step_by_step_tour | boolean | | Yes |
-1
View File
@@ -1566,7 +1566,6 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication.
| enable_email_password_login | boolean, <br>**Default:** true | | Yes |
| enable_explore_banner | boolean | | Yes |
| enable_learn_app | boolean, <br>**Default:** true | | Yes |
| enable_license_expiry_notice | boolean, <br>**Default:** true | | Yes |
| enable_marketplace | boolean | | Yes |
| enable_social_oauth_login | boolean | | Yes |
| enable_step_by_step_tour | boolean | | Yes |
+1 -1
View File
@@ -89,6 +89,7 @@ class LicenseModel(LicenseStatusModel):
expired_at: str = ""
workspaces: LicenseLimitationModel = LicenseLimitationModel(enabled=False, size=0, limit=0)
seats: LicenseLimitationModel = LicenseLimitationModel(enabled=False, size=0, limit=0)
license_expiry_notice_enabled: bool = False
class BrandingModel(FeatureResponseModel):
@@ -197,4 +198,3 @@ class SystemFeatureModel(FeatureResponseModel):
enable_step_by_step_tour: bool = False
rbac_enabled: bool = False
knowledge_fs_enabled: bool = False
enable_license_expiry_notice: bool = True
+6 -4
View File
@@ -151,9 +151,12 @@ class FeatureService:
Non-enterprise deployments have no license, so an unconstrained default
(unlimited seats/workspaces) is returned.
"""
if not dify_config.ENTERPRISE_ENABLED:
return feature_entities.LicenseModel()
return cls._build_license(EnterpriseService.get_info())
if dify_config.ENTERPRISE_ENABLED:
license_model = cls._build_license(EnterpriseService.get_info())
license_model.license_expiry_notice_enabled = dify_config.ENABLE_LICENSE_EXPIRY_NOTICE
else:
license_model = feature_entities.LicenseModel()
return license_model
@staticmethod
def is_explore_banner_enabled() -> bool:
@@ -173,7 +176,6 @@ class FeatureService:
system_features.webapp_auth.allow_public_access = dify_config.WEBAPP_PUBLIC_ACCESS_ENABLED
system_features.enable_step_by_step_tour = dify_config.ENABLE_STEP_BY_STEP_TOUR
system_features.knowledge_fs_enabled = dify_config.KNOWLEDGE_FS_ENABLED
system_features.enable_license_expiry_notice = dify_config.ENABLE_LICENSE_EXPIRY_NOTICE
@classmethod
def _fulfill_trial_models_from_env(cls) -> list[str]:
@@ -1,21 +1,45 @@
import pytest
from enums.deployment_edition import DeploymentEdition
from services import feature_service as feature_service_module
from services.entities.feature_entities import SystemFeatureModel
from services.entities.feature_entities import LicenseModel, LicenseStatus
from services.feature_service import FeatureService
_ENTERPRISE_INFO = {"License": {"status": LicenseStatus.EXPIRING, "expiredAt": "2026-12-31"}}
def test_system_feature_model_defaults_enable_license_expiry_notice() -> None:
system_features = SystemFeatureModel(deployment_edition=DeploymentEdition.COMMUNITY)
assert system_features.enable_license_expiry_notice is True
def test_license_model_defaults_license_expiry_notice_disabled() -> None:
"""Without a license there is no expiry to announce, so the notice is off unless enabled explicitly."""
assert LicenseModel().license_expiry_notice_enabled is False
@pytest.mark.parametrize("enabled", [True, False])
def test_get_system_features_reads_enable_license_expiry_notice(monkeypatch: pytest.MonkeyPatch, enabled: bool) -> None:
def test_get_license_non_enterprise_ignores_expiry_notice_config(
monkeypatch: pytest.MonkeyPatch, enabled: bool
) -> None:
"""Non-enterprise deployments have no license, so the env toggle never turns the notice on."""
monkeypatch.setattr(feature_service_module.dify_config, "ENABLE_LICENSE_EXPIRY_NOTICE", enabled)
monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", False)
result = FeatureService.get_system_features()
result = FeatureService.get_license()
assert result.enable_license_expiry_notice is enabled
assert result.license_expiry_notice_enabled is False
@pytest.mark.parametrize("enabled", [True, False])
def test_get_license_enterprise_reads_license_expiry_notice_enabled(
monkeypatch: pytest.MonkeyPatch, enabled: bool
) -> None:
"""The enterprise-sourced license carries the env-resolved notice flag alongside its real status."""
monkeypatch.setattr(feature_service_module.dify_config, "ENABLE_LICENSE_EXPIRY_NOTICE", enabled)
monkeypatch.setattr("services.feature_service.dify_config.ENTERPRISE_ENABLED", True)
monkeypatch.setattr(
feature_service_module.EnterpriseService,
"get_info",
staticmethod(lambda: _ENTERPRISE_INFO),
)
result = FeatureService.get_license()
assert result.status == LicenseStatus.EXPIRING
assert result.expired_at == "2026-12-31"
assert result.license_expiry_notice_enabled is enabled
@@ -15,7 +15,6 @@ export type SystemFeatureModel = {
enable_email_password_login: boolean
enable_explore_banner: boolean
enable_learn_app: boolean
enable_license_expiry_notice: boolean
enable_marketplace: boolean
enable_social_oauth_login: boolean
enable_step_by_step_tour: boolean
@@ -32,6 +31,7 @@ export type SystemFeatureModel = {
export type LicenseModel = {
expired_at: string
license_expiry_notice_enabled: boolean
seats: LicenseLimitationModel
status: LicenseStatus
workspaces: LicenseLimitationModel
@@ -48,6 +48,7 @@ export const zLicenseStatus = z.enum(['active', 'expired', 'expiring', 'inactive
*/
export const zLicenseModel = z.object({
expired_at: z.string().default(''),
license_expiry_notice_enabled: z.boolean().default(false),
seats: zLicenseLimitationModel.default({
enabled: false,
limit: 0,
@@ -127,7 +128,6 @@ export const zSystemFeatureModel = z.object({
enable_email_password_login: z.boolean().default(true),
enable_explore_banner: z.boolean().default(false),
enable_learn_app: z.boolean().default(true),
enable_license_expiry_notice: z.boolean().default(true),
enable_marketplace: z.boolean().default(false),
enable_social_oauth_login: z.boolean().default(false),
enable_step_by_step_tour: z.boolean().default(false),
@@ -520,7 +520,6 @@ export type SystemFeatureModel = {
enable_email_password_login: boolean
enable_explore_banner: boolean
enable_learn_app: boolean
enable_license_expiry_notice: boolean
enable_marketplace: boolean
enable_social_oauth_login: boolean
enable_step_by_step_tour: boolean
@@ -788,7 +788,6 @@ export const zSystemFeatureModel = z.object({
enable_email_password_login: z.boolean().default(true),
enable_explore_banner: z.boolean().default(false),
enable_learn_app: z.boolean().default(true),
enable_license_expiry_notice: z.boolean().default(true),
enable_marketplace: z.boolean().default(false),
enable_social_oauth_login: z.boolean().default(false),
enable_step_by_step_tour: z.boolean().default(false),
@@ -1,4 +1,3 @@
import type { ConsoleQueryTestOptions } from '@/test/console/query-data'
import { zLicenseStatus } from '@dify/contracts/api/console/system-features/zod.gen'
import { screen } from '@testing-library/react'
import dayjs from 'dayjs'
@@ -10,10 +9,7 @@ import {
} from '@/test/console/query-data'
import LicenseBadge from '../index'
const renderLicenseBadge = (
license?: Parameters<typeof seedSystemFeaturesLicense>[1],
systemFeatures?: ConsoleQueryTestOptions['systemFeatures'],
) => {
const renderLicenseBadge = (license?: Parameters<typeof seedSystemFeaturesLicense>[1]) => {
const queryClient = createConsoleQueryClient()
if (license) seedSystemFeaturesLicense(queryClient, license)
else {
@@ -22,7 +18,7 @@ const renderLicenseBadge = (
queryFn: () => new Promise(() => {}),
})
}
return renderWithConsoleQuery(<LicenseBadge />, { queryClient, systemFeatures })
return renderWithConsoleQuery(<LicenseBadge />, { queryClient })
}
describe('LicenseBadge', () => {
@@ -54,40 +50,53 @@ describe('LicenseBadge', () => {
it('should render singular expiring message when license expires in 0 days', () => {
const expiredAt = dayjs().add(2, 'hours').toISOString()
renderLicenseBadge({ status: zLicenseStatus.enum.expiring, expired_at: expiredAt })
renderLicenseBadge({
status: zLicenseStatus.enum.expiring,
expired_at: expiredAt,
license_expiry_notice_enabled: true,
})
expect(screen.getByText(/license\.expiring/)).toBeInTheDocument()
expect(screen.getByText(/count":0/)).toBeInTheDocument()
})
it('should render singular expiring message when license expires in 1 day', () => {
const tomorrow = dayjs().add(1, 'day').add(1, 'hour').toISOString()
renderLicenseBadge({ status: zLicenseStatus.enum.expiring, expired_at: tomorrow })
renderLicenseBadge({
status: zLicenseStatus.enum.expiring,
expired_at: tomorrow,
license_expiry_notice_enabled: true,
})
expect(screen.getByText(/license\.expiring/)).toBeInTheDocument()
expect(screen.getByText(/count":1/)).toBeInTheDocument()
})
it('should render plural expiring message when license expires in 5 days', () => {
const fiveDaysLater = dayjs().add(5, 'day').add(1, 'hour').toISOString()
renderLicenseBadge({ status: zLicenseStatus.enum.expiring, expired_at: fiveDaysLater })
renderLicenseBadge({
status: zLicenseStatus.enum.expiring,
expired_at: fiveDaysLater,
license_expiry_notice_enabled: true,
})
expect(screen.getByText(/license\.expiring_plural/)).toBeInTheDocument()
expect(screen.getByText(/count":5/)).toBeInTheDocument()
})
it('should fall back to the Enterprise badge when the expiry notice is disabled', () => {
const fiveDaysLater = dayjs().add(5, 'day').add(1, 'hour').toISOString()
renderLicenseBadge(
{ status: zLicenseStatus.enum.expiring, expired_at: fiveDaysLater },
{ enable_license_expiry_notice: false },
)
renderLicenseBadge({
status: zLicenseStatus.enum.expiring,
expired_at: fiveDaysLater,
license_expiry_notice_enabled: false,
})
expect(screen.queryByText(/license\.expiring/)).not.toBeInTheDocument()
expect(screen.getByText('Enterprise')).toBeInTheDocument()
})
it('should keep rendering the Enterprise badge for an active license when the expiry notice is disabled', () => {
renderLicenseBadge(
{ status: zLicenseStatus.enum.active },
{ enable_license_expiry_notice: false },
)
renderLicenseBadge({
status: zLicenseStatus.enum.active,
license_expiry_notice_enabled: false,
})
expect(screen.getByText('Enterprise')).toBeInTheDocument()
})
})
@@ -1,23 +1,18 @@
'use client'
import { zLicenseStatus } from '@dify/contracts/api/console/system-features/zod.gen'
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { useQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { consoleQuery } from '@/service/client'
import PremiumBadge from '../../base/premium-badge'
function LicenseBadge() {
const { t } = useTranslation()
const { data: license } = useQuery(consoleQuery.systemFeatures.license.get.queryOptions())
const { data: expiryNoticeEnabled } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
select: ({ enable_license_expiry_notice }) => enable_license_expiry_notice,
})
const isExpiring = license?.status === zLicenseStatus.enum.expiring
if (isExpiring && expiryNoticeEnabled) {
if (isExpiring && license.license_expiry_notice_enabled) {
const count = dayjs(license.expired_at).diff(dayjs(), 'days')
return (
<PremiumBadge color="orange" className="select-none">
+1 -1
View File
@@ -57,11 +57,11 @@ const baseSystemFeatures = {
enable_learn_app: true,
enable_step_by_step_tour: false,
knowledge_fs_enabled: false,
enable_license_expiry_notice: true,
} satisfies GetSystemFeaturesResponse
const baseSystemFeaturesLicense = {
status: zLicenseStatus.enum.none,
license_expiry_notice_enabled: false,
expired_at: '',
workspaces: {
enabled: false,