From d0a56cdcac6c8df6e736cbfc7e338b93dd36f3a8 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 14 Aug 2026 07:16:21 +0000 Subject: [PATCH] chore: reopen the education application flow (#40760) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> (cherry picked from commit 35132d6892349a9ed6ebe07d8f86e058c1c62d81) --- api/controllers/console/error.py | 6 ---- api/controllers/console/workspace/account.py | 13 ++++----- .../console/test_workspace_account.py | 23 +++++++-------- .../billing/plan/__tests__/index.spec.tsx | 23 +++++---------- web/app/components/billing/plan/index.tsx | 29 ------------------- web/i18n/ar-TN/education.json | 4 --- web/i18n/de-DE/education.json | 4 --- web/i18n/en-US/education.json | 4 --- web/i18n/es-ES/education.json | 4 --- web/i18n/fa-IR/education.json | 4 --- web/i18n/fr-FR/education.json | 4 --- web/i18n/hi-IN/education.json | 4 --- web/i18n/id-ID/education.json | 4 --- web/i18n/it-IT/education.json | 4 --- web/i18n/ja-JP/education.json | 4 --- web/i18n/ko-KR/education.json | 4 --- web/i18n/nl-NL/education.json | 4 --- web/i18n/pl-PL/education.json | 4 --- web/i18n/pt-BR/education.json | 4 --- web/i18n/ro-RO/education.json | 4 --- web/i18n/ru-RU/education.json | 4 --- web/i18n/sl-SI/education.json | 4 --- web/i18n/th-TH/education.json | 4 --- web/i18n/tr-TR/education.json | 4 --- web/i18n/uk-UA/education.json | 4 --- web/i18n/vi-VN/education.json | 4 --- web/i18n/zh-Hans/education.json | 4 --- web/i18n/zh-Hant/education.json | 4 --- 28 files changed, 24 insertions(+), 162 deletions(-) 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)} - />