diff --git a/api/controllers/console/error.py b/api/controllers/console/error.py
index 638af52e284..e4352f92f88 100644
--- a/api/controllers/console/error.py
+++ b/api/controllers/console/error.py
@@ -109,12 +109,6 @@ class EducationActivateLimitError(BaseHTTPException):
code = 429
-class EducationDiscountTemporarilyPausedError(BaseHTTPException):
- error_code = "education_discount_temporarily_paused"
- description = "Education discount temporarily paused, while we upgrade our security measures."
- code = 503
-
-
class ComplianceRateLimitError(BaseHTTPException):
error_code = "compliance_rate_limit"
description = "Rate limit exceeded for downloading compliance report."
diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py
index 4434b7080f5..a37476953c4 100644
--- a/api/controllers/console/workspace/account.py
+++ b/api/controllers/console/workspace/account.py
@@ -28,12 +28,7 @@ from controllers.console.auth.error import (
InvalidEmailError,
InvalidTokenError,
)
-from controllers.console.error import (
- AccountInFreezeError,
- AccountNotFound,
- EducationDiscountTemporarilyPausedError,
- EmailSendIpLimitError,
-)
+from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
from controllers.console.workspace.error import (
AccountAlreadyInitedError,
CurrentPasswordIncorrectError,
@@ -566,7 +561,11 @@ class EducationApi(Resource):
@cloud_edition_billing_enabled
@with_current_user
def post(self, account: Account):
- raise EducationDiscountTemporarilyPausedError()
+ payload = console_ns.payload or {}
+ args = EducationActivatePayload.model_validate(payload)
+
+ result = BillingService.EducationIdentity.activate(account, args.token, args.institution, args.role)
+ return result
@setup_required
@login_required
diff --git a/api/tests/unit_tests/controllers/console/test_workspace_account.py b/api/tests/unit_tests/controllers/console/test_workspace_account.py
index ce10cc9e070..fce2993a815 100644
--- a/api/tests/unit_tests/controllers/console/test_workspace_account.py
+++ b/api/tests/unit_tests/controllers/console/test_workspace_account.py
@@ -8,7 +8,6 @@ import pytest
from flask import Flask
from sqlalchemy.orm import Session, scoped_session, sessionmaker
-from controllers.console.error import EducationDiscountTemporarilyPausedError
from controllers.console.workspace.account import (
AccountDeleteUpdateFeedbackApi,
ChangeEmailCheckApi,
@@ -110,23 +109,21 @@ def _build_change_email_token(
class TestEducationApi:
@patch("controllers.console.workspace.account.BillingService.EducationIdentity.activate")
- def test_post_returns_temporarily_paused_error_without_activating_discount(
- self, mock_activate: MagicMock, app: Flask
- ):
+ def test_post_activates_education_discount(self, mock_activate: MagicMock, app: Flask):
account = _build_account("student@example.edu")
+ mock_activate.return_value = {"message": "success"}
- with app.test_request_context("/account/education", method="POST", json={}):
+ with app.test_request_context(
+ "/account/education",
+ method="POST",
+ json={"token": "education-token", "institution": "Dify University", "role": "Student"},
+ ):
api = EducationApi()
method = inspect.unwrap(api.post)
- with pytest.raises(EducationDiscountTemporarilyPausedError) as exc_info:
- method(api, account)
+ result = method(api, account)
- assert exc_info.value.data == {
- "code": "education_discount_temporarily_paused",
- "message": "Education discount temporarily paused, while we upgrade our security measures.",
- "status": 503,
- }
- mock_activate.assert_not_called()
+ assert result == {"message": "success"}
+ mock_activate.assert_called_once_with(account, "education-token", "Dify University", "Student")
class TestChangeEmailSend:
diff --git a/web/app/components/billing/plan/__tests__/index.spec.tsx b/web/app/components/billing/plan/__tests__/index.spec.tsx
index eb1ee3a0e7e..f1d6215ad31 100644
--- a/web/app/components/billing/plan/__tests__/index.spec.tsx
+++ b/web/app/components/billing/plan/__tests__/index.spec.tsx
@@ -1,4 +1,4 @@
-import { screen, within } from '@testing-library/react'
+import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { baseProviderContextValue } from '@/context/provider-context'
import { createConsoleQueryWrapper } from '@/test/console/query-data'
@@ -102,7 +102,7 @@ const renderPlan = (educationStatus = { allow_refresh: false, is_student: false
return render(, { wrapper })
}
-describe('PlanComp education discount pause', () => {
+describe('PlanComp education discount', () => {
beforeEach(() => {
vi.clearAllMocks()
})
@@ -124,23 +124,16 @@ describe('PlanComp education discount pause', () => {
expect(screen.queryByRole('button', { name: 'education.toVerified' })).not.toBeInTheDocument()
})
- it('shows the pause notice instead of starting verification and closes it with OK', async () => {
+ it('starts education verification and opens the application form', async () => {
const user = userEvent.setup()
+ mocks.mutateAsync.mockResolvedValue({ token: 'education-token' })
renderPlan()
await user.click(screen.getByRole('button', { name: 'education.toVerified' }))
- const dialog = await screen.findByRole('dialog')
- expect(dialog).toHaveTextContent('education.educationDiscountPaused.title')
- expect(dialog).toHaveTextContent('education.educationDiscountPaused.description')
- expect(dialog).toHaveTextContent('education.educationDiscountPaused.thanks')
- expect(dialog).toHaveTextContent('education.educationDiscountPaused.publishedAt')
- expect(within(dialog).getAllByRole('button')).toHaveLength(1)
- expect(within(dialog).getByRole('button')).toHaveAccessibleName('common.operation.ok')
- expect(mocks.mutateAsync).not.toHaveBeenCalled()
-
- await user.click(screen.getByRole('button', { name: 'common.operation.ok' }))
-
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
+ expect(mocks.mutateAsync).toHaveBeenCalledOnce()
+ await waitFor(() => {
+ expect(mocks.push).toHaveBeenCalledWith('/education-apply?token=education-token')
+ })
})
})
diff --git a/web/app/components/billing/plan/index.tsx b/web/app/components/billing/plan/index.tsx
index 63451476908..d65c8cf1766 100644
--- a/web/app/components/billing/plan/index.tsx
+++ b/web/app/components/billing/plan/index.tsx
@@ -33,9 +33,6 @@ type Props = Readonly<{
loc: string
}>
-// TODO: Remove this temporary gate once education applications and redemptions reopen.
-const EDUCATION_DISCOUNT_TEMPORARILY_PAUSED = true
-
const PlanComp: FC = ({ loc }) => {
const { t } = useTranslation()
const { data: deploymentEdition } = useSuspenseQuery({
@@ -66,19 +63,12 @@ const PlanComp: FC = ({ loc }) => {
})()
const [showModal, setShowModal] = React.useState(false)
- const [showEducationDiscountPausedModal, setShowEducationDiscountPausedModal] =
- React.useState(false)
const { handleEducationDiscount, isEducationDiscountLoading } = useEducationDiscount()
const { mutateAsync, isPending } = useEducationVerify()
const setShowAccountSettingModal = useModalContextSelector((s) => s.setShowAccountSettingModal)
const setEducationVerifying = useSetEducationVerifying()
const unmountedRef = useUnmountedRef()
const handleVerify = () => {
- if (EDUCATION_DISCOUNT_TEMPORARILY_PAUSED) {
- setShowEducationDiscountPausedModal(true)
- return
- }
-
if (isPending) return
mutateAsync()
.then((res) => {
@@ -184,25 +174,6 @@ const PlanComp: FC = ({ loc }) => {
resetInDays={apiRateLimitResetInDays}
/>
- $['educationDiscountPaused.title'], { ns: 'education' })}
- content={
- <>
-
- {t(($) => $['educationDiscountPaused.description'], { ns: 'education' })}
-
-
- {t(($) => $['educationDiscountPaused.thanks'], { ns: 'education' })}
-
-
- {t(($) => $['educationDiscountPaused.publishedAt'], { ns: 'education' })}
-
- >
- }
- onConfirm={() => setShowEducationDiscountPausedModal(false)}
- onCancel={() => setShowEducationDiscountPausedModal(false)}
- />