Yuki Takei 8 месяцев назад
Родитель
Сommit
ffe2725324
34 измененных файлов с 0 добавлено и 2632 удалено
  1. 0 152
      apps/app/src/features/questionnaire/client/components/ProactiveQuestionnaireModal.tsx
  2. 0 52
      apps/app/src/features/questionnaire/client/components/Question.tsx
  3. 0 167
      apps/app/src/features/questionnaire/client/components/QuestionnaireModal.tsx
  4. 0 9
      apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.module.scss
  5. 0 51
      apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.tsx
  6. 0 85
      apps/app/src/features/questionnaire/client/components/QuestionnaireToast.tsx
  7. 0 79
      apps/app/src/features/questionnaire/client/services/guest-questionnaire-answer-status.ts
  8. 0 40
      apps/app/src/features/questionnaire/client/stores/model.tsx
  9. 0 24
      apps/app/src/features/questionnaire/client/stores/questionnaire.tsx
  10. 0 4
      apps/app/src/features/questionnaire/interfaces/answer.ts
  11. 0 25
      apps/app/src/features/questionnaire/interfaces/condition.ts
  12. 0 27
      apps/app/src/features/questionnaire/interfaces/proactive-questionnaire-answer.ts
  13. 0 15
      apps/app/src/features/questionnaire/interfaces/question.ts
  14. 0 16
      apps/app/src/features/questionnaire/interfaces/questionnaire-answer-status.ts
  15. 0 21
      apps/app/src/features/questionnaire/interfaces/questionnaire-answer.ts
  16. 0 21
      apps/app/src/features/questionnaire/interfaces/questionnaire-order.ts
  17. 0 12
      apps/app/src/features/questionnaire/interfaces/user-info.ts
  18. 0 28
      apps/app/src/features/questionnaire/server/models/proactive-questionnaire-answer.ts
  19. 0 19
      apps/app/src/features/questionnaire/server/models/questionnaire-answer-status.ts
  20. 0 25
      apps/app/src/features/questionnaire/server/models/questionnaire-answer.ts
  21. 0 35
      apps/app/src/features/questionnaire/server/models/questionnaire-order.ts
  22. 0 10
      apps/app/src/features/questionnaire/server/models/schema/answer.ts
  23. 0 29
      apps/app/src/features/questionnaire/server/models/schema/condition.ts
  24. 0 42
      apps/app/src/features/questionnaire/server/models/schema/growi-info.ts
  25. 0 16
      apps/app/src/features/questionnaire/server/models/schema/question.ts
  26. 0 10
      apps/app/src/features/questionnaire/server/models/schema/user-info.ts
  27. 0 384
      apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts
  28. 0 515
      apps/app/src/features/questionnaire/server/service/questionnaire-cron.integ.ts
  29. 0 106
      apps/app/src/features/questionnaire/server/service/questionnaire-cron.ts
  30. 0 301
      apps/app/src/features/questionnaire/server/service/questionnaire.integ.ts
  31. 0 74
      apps/app/src/features/questionnaire/server/service/questionnaire.ts
  32. 0 70
      apps/app/src/features/questionnaire/server/util/condition.ts
  33. 0 128
      apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.spec.ts
  34. 0 40
      apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.ts

+ 0 - 152
apps/app/src/features/questionnaire/client/components/ProactiveQuestionnaireModal.tsx

@@ -1,152 +0,0 @@
-import { useState, useCallback, type JSX } from 'react';
-
-import { useTranslation } from 'next-i18next';
-import {
-  Modal, ModalBody,
-} from 'reactstrap';
-
-import { apiv3Post } from '~/client/util/apiv3-client';
-
-type ModalProps = {
-  isOpen: boolean,
-  onClose: () => void,
-};
-
-const QuestionnaireCompletionModal = (props: ModalProps): JSX.Element => {
-  const { t } = useTranslation('commons');
-
-  const { isOpen, onClose } = props;
-
-  return (
-    <Modal
-      size="lg"
-      isOpen={isOpen}
-      toggle={onClose}
-      centered
-    >
-      <ModalBody className="overflow-hidden p-0" style={{ borderRadius: 8 }}>
-        <div className="m-2 p-4" style={{ borderRadius: 8 }}>
-          <div className="text-center">
-            <h2 className="my-4">{t('questionnaire_modal.title')}</h2>
-            <p className="mb-1">{t('questionnaire_modal.successfully_submitted')}</p>
-            <p>{t('questionnaire_modal.thanks_for_answering')}</p>
-          </div>
-          <div className="text-center my-3">
-            <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={onClose}>{t('Close')}</span>
-          </div>
-        </div>
-      </ModalBody>
-    </Modal>
-  );
-};
-
-const ProactiveQuestionnaireModal = (props: ModalProps): JSX.Element => {
-  const { t } = useTranslation('commons');
-
-  const { isOpen, onClose } = props;
-
-  const [isQuestionnaireCompletionModal, setQuestionnaireCompletionModal] = useState(false);
-
-  const submitHandler = useCallback(async(e) => {
-    e.preventDefault();
-
-    const formData = e.target.elements;
-
-    const {
-      satisfaction: { value: satisfaction },
-      lengthOfExperience: { value: lengthOfExperience },
-      occupation: { value: occupation },
-      position: { value: position },
-      commentText: { value: commentText },
-    } = formData;
-
-    const sendValues = {
-      satisfaction: Number(satisfaction),
-      lengthOfExperience,
-      occupation,
-      position,
-      commentText,
-    };
-
-    apiv3Post('/questionnaire/proactive/answer', sendValues);
-
-    onClose();
-    setQuestionnaireCompletionModal(true);
-  }, [onClose]);
-
-  return (
-    <>
-      <Modal
-        data-testid="grw-proactive-questionnaire-modal"
-        size="lg"
-        isOpen={isOpen}
-        toggle={onClose}
-        centered
-      >
-        <ModalBody className="overflow-hidden p-0" style={{ borderRadius: 8 }}>
-          <div className="m-2 p-4" style={{ borderRadius: 8 }}>
-            <div className="text-center">
-              <h2 className="my-4">{t('questionnaire_modal.title')}</h2>
-              <p className="mb-1">{t('questionnaire_modal.more_satisfied_services')}</p>
-              <p>{t('questionnaire_modal.strive_to_improve_services')}</p>
-            </div>
-            <form className="px-5" onSubmit={submitHandler}>
-              <div className="row mt-5">
-                <label className="col-sm-5 col-form-label" htmlFor="satisfaction">
-                  <span className="badge bg-primary me-2">{t('questionnaire_modal.required')}</span>{t('questionnaire_modal.satisfaction_with_growi')}
-                </label>
-                <select className="col-sm-7 form-control" name="satisfaction" id="satisfaction" required>
-                  <option value="">▼ {t('Select')}</option>
-                  <option>1</option>
-                  <option>2</option>
-                  <option>3</option>
-                  <option>4</option>
-                  <option>5</option>
-                </select>
-              </div>
-              <div className="row mt-3">
-                <label className="col-sm-5 col-form-label" htmlFor="lengthOfExperience">{t('questionnaire_modal.history_of_growi_usage')}</label>
-                <select
-                  name="lengthOfExperience"
-                  id="lengthOfExperience"
-                  className="col-sm-7 form-control"
-                >
-                  <option value="">▼ {t('Select')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.more_than_two_years')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.one_to_two_years')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.six_months_to_one_year')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.three_months_to_six_months')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.one_month_to_three_months')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.less_than_one_month')}</option>
-                </select>
-              </div>
-              <div className="row mt-3">
-                <label className="col-sm-5 col-form-label" htmlFor="occupation">{t('questionnaire_modal.occupation')}</label>
-                <input className="col-sm-7 form-control" type="text" name="occupation" id="occupation" />
-              </div>
-              <div className="row mt-3">
-                <label className="col-sm-5 col-form-label" htmlFor="position">{t('questionnaire_modal.position')}</label>
-                <input className="col-sm-7 form-control" type="text" name="position" id="position" />
-              </div>
-              <div className="row mt-3">
-                <label className="col-sm-5 col-form-label" htmlFor="commentText">
-                  <span className="badge bg-primary me-2">{t('questionnaire_modal.required')}</span>{t('questionnaire_modal.comment_on_growi')}
-                </label>
-                <textarea className="col-sm-7 form-control" name="commentText" id="commentText" rows={5} required />
-              </div>
-              <div className="text-center mt-5">
-                <button type="submit" className="btn btn-primary">{t('questionnaire_modal.submit')}</button>
-              </div>
-              <div className="text-center my-3">
-                <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={onClose}>{t('questionnaire_modal.close')}</span>
-              </div>
-            </form>
-          </div>
-        </ModalBody>
-      </Modal>
-      <QuestionnaireCompletionModal isOpen={isQuestionnaireCompletionModal} onClose={() => setQuestionnaireCompletionModal(false)} />
-    </>
-  );
-};
-
-export default ProactiveQuestionnaireModal;

+ 0 - 52
apps/app/src/features/questionnaire/client/components/Question.tsx

@@ -1,52 +0,0 @@
-import type { JSX } from 'react';
-
-import { useCurrentUser } from '~/stores-universal/context';
-
-import type { IQuestionHasId } from '../../interfaces/question';
-
-
-type QuestionProps = {
-  question: IQuestionHasId,
-  inputNamePrefix: string,
-}
-
-const Question = ({ question, inputNamePrefix }: QuestionProps): JSX.Element => {
-  const { data: currentUser } = useCurrentUser();
-  const lang = currentUser?.lang;
-
-  const questionText = lang === 'en_US' ? question.text.en_US : question.text.ja_JP;
-
-  return (
-    <div className="row mt-4">
-      <div className="col-6 d-flex align-items-center">
-        <span>
-          {questionText}
-        </span>
-      </div>
-      <div className="col-6 d-flex align-items-center ps-0">
-        <div className="btn-group flex-fill grw-questionnaire-btn-group">
-          <label className="form-label btn btn-outline-primary active me-4 rounded">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-noAnswer`} value="0" defaultChecked /> -
-          </label>
-          <label className="form-label btn btn-outline-primary rounded-start">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option1`} value="1" /> 1
-          </label>
-          <label className="form-label btn btn-outline-primary">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option2`} value="2" /> 2
-          </label>
-          <label className="form-label btn btn-outline-primary">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option3`} value="3" /> 3
-          </label>
-          <label className="form-label btn btn-outline-primary">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option4`} value="4" /> 4
-          </label>
-          <label className="form-label btn btn-outline-primary">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option5`} value="5" /> 5
-          </label>
-        </div>
-      </div>
-    </div>
-  );
-};
-
-export default Question;

+ 0 - 167
apps/app/src/features/questionnaire/client/components/QuestionnaireModal.tsx

@@ -1,167 +0,0 @@
-import { useCallback, type JSX } from 'react';
-
-import { useTranslation } from 'next-i18next';
-import { Modal, ModalBody } from 'reactstrap';
-
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useQuestionnaireModal } from '~/features/questionnaire/client/stores/model';
-import type { IAnswer } from '~/features/questionnaire/interfaces/answer';
-import { StatusType } from '~/features/questionnaire/interfaces/questionnaire-answer-status';
-import type { IQuestionnaireOrderHasId } from '~/features/questionnaire/interfaces/questionnaire-order';
-import { useCurrentUser } from '~/stores-universal/context';
-import loggerFactory from '~/utils/logger';
-
-import { GuestQuestionnaireAnswerStatusService } from '../services/guest-questionnaire-answer-status';
-
-import Question from './Question';
-
-const logger = loggerFactory('growi:QuestionnaireModal');
-
-type QuestionnaireModalProps = {
-  questionnaireOrder: IQuestionnaireOrderHasId
-}
-
-const QuestionnaireModal = ({ questionnaireOrder }: QuestionnaireModalProps): JSX.Element => {
-  const { data: currentUser } = useCurrentUser();
-  const lang = currentUser?.lang;
-
-  const { data: questionnaireModalData, close: closeQuestionnaireModal } = useQuestionnaireModal();
-  const isOpened = questionnaireModalData?.openedQuestionnaireId === questionnaireOrder._id;
-
-  const inputNamePrefix = 'question-';
-
-  const { t } = useTranslation(['translation', 'commons']);
-
-  const sendAnswer = useCallback(async(answers: IAnswer[]) => {
-    try {
-      await apiv3Put('/questionnaire/answer', {
-        questionnaireOrderId: questionnaireOrder._id,
-        answers,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.answered);
-      }
-      toastSuccess(
-        <>
-          <div className="fw-bold">{t('questionnaire.thank_you_for_answering')}</div>
-          <div className="pt-2">{t('questionnaire.additional_feedback')}</div>
-        </>,
-        {
-          autoClose: 3000,
-          closeButton: true,
-        },
-      );
-    }
-    catch (e) {
-      logger.error(e);
-      toastError(t('questionnaire.failed_to_send'));
-    }
-  }, [questionnaireOrder._id, t, currentUser]);
-
-  const submitHandler = useCallback(async(event) => {
-    event.preventDefault();
-
-    const answers: IAnswer[] = questionnaireOrder.questions.map((question) => {
-      const answerValue = event.target[`${inputNamePrefix + question._id}`].value;
-      return { question: question._id, value: answerValue };
-    });
-
-    sendAnswer(answers);
-
-    const shouldCloseToast = true;
-    closeQuestionnaireModal(shouldCloseToast);
-  }, [closeQuestionnaireModal, questionnaireOrder.questions, sendAnswer]);
-
-  const denyBtnClickHandler = useCallback(async() => {
-    try {
-      apiv3Put('/questionnaire/deny', {
-        questionnaireOrderId: questionnaireOrder._id,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.denied);
-      }
-      toastSuccess(t('questionnaire.denied'));
-    }
-    catch (e) {
-      logger.error(e);
-    }
-    const shouldCloseToast = true;
-    closeQuestionnaireModal(shouldCloseToast);
-  }, [closeQuestionnaireModal, questionnaireOrder._id, t, currentUser]);
-
-  // No showing toasts since not important
-  const closeBtnClickHandler = useCallback(async(shouldCloseToast: boolean) => {
-    closeQuestionnaireModal(shouldCloseToast);
-
-    try {
-      await apiv3Put('/questionnaire/skip', {
-        questionnaireOrderId: questionnaireOrder._id,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.skipped);
-      }
-    }
-    catch (e) {
-      logger.error(e);
-    }
-  }, [closeQuestionnaireModal, questionnaireOrder._id, currentUser]);
-
-  const closeBtnClickHandlerClosingToast = useCallback(async() => {
-    closeBtnClickHandler(true);
-  }, [closeBtnClickHandler]);
-
-  const questionnaireOrderTitle = lang === 'en_US' ? questionnaireOrder.title.en_US : questionnaireOrder.title.ja_JP;
-
-  return (
-    <Modal
-      size="lg"
-      isOpen={isOpened}
-      toggle={closeBtnClickHandlerClosingToast}
-      centered
-    >
-      <form onSubmit={submitHandler}>
-        <ModalBody className="bg-primary overflow-hidden p-0" style={{ borderRadius: 8 }}>
-          <div className="bg-white m-2 p-4" style={{ borderRadius: 8 }}>
-            <div className="text-center mb-2">
-              <h2 className="my-4">{questionnaireOrderTitle}</h2>
-              <p className="mb-1">{t('commons:questionnaire_modal.more_satisfied_services')}</p>
-              <p>{t('commons:questionnaire_modal.strive_to_improve_services')}</p>
-            </div>
-            <div className="container">
-              <div className="row mt-4">
-                <div className="col-md-2 offset-md-5 fw-bold text-end align-items-center p-0">{t('questionnaire.no_answer')}</div>
-                <div className="col-md-5 d-flex justify-content-between align-items-center">
-                  <span className="fw-bold">{t('questionnaire.disagree')}</span>
-                  <span className="fw-bold">{t('questionnaire.agree')}</span>
-                </div>
-              </div>
-              {questionnaireOrder.questions?.map((question) => {
-                return <Question question={question} inputNamePrefix={inputNamePrefix} key={question._id} />;
-              })}
-            </div>
-            <div className="text-center mt-5">
-              <button type="submit" className="btn btn-primary">{t('questionnaire.answer')}</button>
-            </div>
-            <div className="text-center cursor-pointer text-decoration-underline my-3">
-              <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={denyBtnClickHandler}>{t('questionnaire.dont_show_again')}</span>
-            </div>
-
-            {currentUser?.admin && (
-              <a href="/admin/app#questionnaire-settings">
-                <i className="material-symbols-outlined me-1">admin_panel_settings</i>
-              </a>
-            )}
-            {currentUser != null && (
-              <a href="/me#other_settings">
-                <i className="material-symbols-outlined">settings</i>
-              </a>
-            )}
-          </div>
-        </ModalBody>
-      </form>
-    </Modal>
-  );
-};
-
-export default QuestionnaireModal;

+ 0 - 9
apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.module.scss

@@ -1,9 +0,0 @@
-@use '@growi/core-styles/scss/bootstrap/init' as bs;
-
-.grw-questionnaire-toasts :global {
-  position: fixed;
-  right: 20px;
-  bottom: 52px;
-  z-index: bs.$zindex-fixed + 1;
-  width: 230px;
-}

+ 0 - 51
apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.tsx

@@ -1,51 +0,0 @@
-import { useCallback, type JSX } from 'react';
-
-import { useCurrentUser } from '~/stores-universal/context';
-
-
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import type { IQuestionnaireOrderHasId } from '../../interfaces/questionnaire-order';
-import { GuestQuestionnaireAnswerStatusService } from '../services/guest-questionnaire-answer-status';
-import { useSWRxQuestionnaireOrders } from '../stores/questionnaire';
-
-import QuestionnaireModal from './QuestionnaireModal';
-import QuestionnaireToast from './QuestionnaireToast';
-
-import styles from './QuestionnaireModalManager.module.scss';
-
-const QuestionnaireModalManager = ():JSX.Element => {
-  const { data: questionnaireOrders } = useSWRxQuestionnaireOrders();
-  const { data: currentUser } = useCurrentUser();
-
-  const questionnaireOrdersToShow = useCallback((questionnaireOrders: IQuestionnaireOrderHasId[] | undefined) => {
-    const guestQuestionnaireAnswerStorage = GuestQuestionnaireAnswerStatusService.getStorage();
-    if (currentUser != null || guestQuestionnaireAnswerStorage == null) {
-      return questionnaireOrders;
-    }
-
-    return questionnaireOrders?.filter((questionnaireOrder) => {
-      const localAnswerStatus = guestQuestionnaireAnswerStorage[questionnaireOrder._id];
-      return localAnswerStatus == null || localAnswerStatus.status === StatusType.not_answered;
-    });
-  }, [currentUser]);
-
-  return (
-    <>
-      {questionnaireOrders?.map((questionnaireOrder) => {
-        return (
-          <QuestionnaireModal
-            questionnaireOrder={questionnaireOrder}
-            key={questionnaireOrder._id}
-          />
-        );
-      })}
-      <div className={styles['grw-questionnaire-toasts']}>
-        {questionnaireOrdersToShow(questionnaireOrders)?.map((questionnaireOrder) => {
-          return <QuestionnaireToast questionnaireOrder={questionnaireOrder} key={questionnaireOrder._id} />;
-        })}
-      </div>
-    </>
-  );
-};
-
-export default QuestionnaireModalManager;

+ 0 - 85
apps/app/src/features/questionnaire/client/components/QuestionnaireToast.tsx

@@ -1,85 +0,0 @@
-import { useCallback, useState, type JSX } from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess } from '~/client/util/toastr';
-import { useQuestionnaireModal } from '~/features/questionnaire/client/stores/model';
-import { useCurrentUser } from '~/stores-universal/context';
-import loggerFactory from '~/utils/logger';
-
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import type { IQuestionnaireOrderHasId } from '../../interfaces/questionnaire-order';
-import { GuestQuestionnaireAnswerStatusService } from '../services/guest-questionnaire-answer-status';
-
-const logger = loggerFactory('growi:QuestionnaireToast');
-
-type QuestionnaireToastProps = {
-  questionnaireOrder: IQuestionnaireOrderHasId,
-}
-
-const QuestionnaireToast = ({ questionnaireOrder }: QuestionnaireToastProps): JSX.Element => {
-  const { open: openQuestionnaireModal } = useQuestionnaireModal();
-  const { data: currentUser } = useCurrentUser();
-  const lang = currentUser?.lang;
-
-  const [isOpen, setIsOpen] = useState(true);
-
-  const { t } = useTranslation();
-
-  const answerBtnClickHandler = useCallback(() => {
-    openQuestionnaireModal(questionnaireOrder._id, () => setIsOpen(false));
-  }, [openQuestionnaireModal, questionnaireOrder._id]);
-
-  const denyBtnClickHandler = useCallback(async() => {
-    // Immediately close
-    setIsOpen(false);
-
-    try {
-      await apiv3Put('/questionnaire/deny', {
-        questionnaireOrderId: questionnaireOrder._id,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.denied);
-      }
-      toastSuccess(t('questionnaire.denied'));
-    }
-    catch (e) {
-      logger.error(e);
-    }
-  }, [questionnaireOrder._id, t, currentUser]);
-
-  // No showing toasts since not important
-  const closeBtnClickHandler = useCallback(async() => {
-    setIsOpen(false);
-
-    try {
-      await apiv3Put('/questionnaire/skip', {
-        questionnaireOrderId: questionnaireOrder._id,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.skipped);
-      }
-    }
-    catch (e) {
-      logger.error(e);
-    }
-  }, [questionnaireOrder._id, currentUser]);
-
-  const questionnaireOrderShortTitle = lang === 'en_US' ? questionnaireOrder.shortTitle.en_US : questionnaireOrder.shortTitle.ja_JP;
-
-  return (
-    <div className={`toast ${isOpen ? 'show' : 'hide'}`}>
-      <div className="toast-header bg-primary">
-        <strong className="me-auto text-light">{questionnaireOrderShortTitle}</strong>
-        <button type="button" className="ms-2 mb-1 btn-close" onClick={closeBtnClickHandler} aria-label="Close"></button>
-      </div>
-      <div className="toast-body bg-light text-dark d-flex justify-content-end">
-        <button type="button" className="btn btn-secondary me-3" onClick={answerBtnClickHandler}>{t('questionnaire.answer')}</button>
-        <button type="button" className="btn btn-secondary" onClick={denyBtnClickHandler}>{t('questionnaire.deny')}</button>
-      </div>
-    </div>
-  );
-};
-
-export default QuestionnaireToast;

+ 0 - 79
apps/app/src/features/questionnaire/client/services/guest-questionnaire-answer-status.ts

@@ -1,79 +0,0 @@
-// A service to manage questionnaire answer statuses for guest user.
-// Saves statuses in localStorage.
-
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-
-interface GuestQuestionnaireAnswerStatus {
-  status: StatusType
-  updatedDate: string
-}
-
-interface GuestQuestionnaireAnswerStatusStorage {
-  [key: string]: GuestQuestionnaireAnswerStatus
-}
-
-const storageKey = 'guestQuestionnaireAnswerStatuses';
-const DAYS_UNTIL_EXPIRATION = 30;
-
-/**
- * Get all answer statuses stored in localStorage as GuestQuestionnaireAnswerStatusStorage,
- * and update outdated information.
- */
-const getStorage = (): GuestQuestionnaireAnswerStatusStorage | null => {
-  if (typeof window === 'undefined') { return null }
-
-  const currentStorage = localStorage.getItem(storageKey);
-
-  if (currentStorage == null) { return null }
-
-  const storageJson: GuestQuestionnaireAnswerStatusStorage = JSON.parse(currentStorage);
-  // delete status if outdated to prevent localStorage overflow
-  // change skipped to not_answered if different date than when skipped
-  Object.keys(storageJson).forEach((key) => {
-    const answerStatus = storageJson[key];
-    const updatedDate = new Date(answerStatus.updatedDate);
-    const expirationDate = new Date(updatedDate.setDate(updatedDate.getDate() + DAYS_UNTIL_EXPIRATION));
-    if (expirationDate < new Date()) {
-      delete storageJson[key];
-    }
-    else if (answerStatus.status === StatusType.skipped
-          && new Date().toDateString() !== answerStatus.updatedDate) {
-      storageJson[key] = {
-        status: StatusType.not_answered,
-        updatedDate: new Date().toDateString(),
-      };
-    }
-  });
-
-  return storageJson;
-};
-
-/**
- * Set answer status for questionnaire order in GuestQuestionnaireAnswerStatusStorage,
- * and save it in localStorage.
- */
-const setStatus = (questionnaireOrderId: string, status: StatusType): void => {
-  if (typeof window === 'undefined') { return }
-
-  const guestQuestionnaireAnswerStatus: GuestQuestionnaireAnswerStatus = {
-    status,
-    updatedDate: new Date().toDateString(),
-  };
-
-  const storage = getStorage();
-
-  if (storage != null) {
-    storage[questionnaireOrderId] = guestQuestionnaireAnswerStatus;
-    localStorage.setItem(storageKey, JSON.stringify(storage));
-    return;
-  }
-
-  const initialStorage: GuestQuestionnaireAnswerStatusStorage = { [questionnaireOrderId]: guestQuestionnaireAnswerStatus };
-  localStorage.setItem(storageKey, JSON.stringify(initialStorage));
-
-};
-
-export const GuestQuestionnaireAnswerStatusService = {
-  setStatus,
-  getStorage,
-};

+ 0 - 40
apps/app/src/features/questionnaire/client/stores/model.tsx

@@ -1,40 +0,0 @@
-import type { SWRResponse } from 'swr';
-
-import { useStaticSWR } from '~/stores/use-static-swr';
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:stores:modal');
-
-/*
-* QuestionnaireModals
-*/
-type QuestionnaireModalStatuses = {
-  openedQuestionnaireId: string | null,
-  closeToast?: () => void | Promise<void>,
-}
-
-type QuestionnaireModalStatusUtils = {
-  open(string: string, closeToast: () => void | Promise<void>): Promise<QuestionnaireModalStatuses | undefined>
-  close(shouldCloseToast?: boolean): Promise<QuestionnaireModalStatuses | undefined>
-}
-
-export const useQuestionnaireModal = (status?: QuestionnaireModalStatuses): SWRResponse<QuestionnaireModalStatuses, Error> & QuestionnaireModalStatusUtils => {
-  const initialData: QuestionnaireModalStatuses = { openedQuestionnaireId: null };
-  const swrResponse = useStaticSWR<QuestionnaireModalStatuses, Error>('questionnaireModalStatus', status, { fallbackData: initialData });
-
-  return {
-    ...swrResponse,
-    open: (questionnaireOrderId: string, closeToast: () => void | Promise<void>) => swrResponse.mutate({
-      openedQuestionnaireId: questionnaireOrderId,
-      closeToast,
-    }),
-    close: (shouldCloseToast?: boolean) => {
-      if (shouldCloseToast) {
-        swrResponse.data?.closeToast?.();
-        if (swrResponse.data?.closeToast === undefined) logger.debug('Tried to run `swrResponse.data?.closeToast` but it was `undefined`');
-      }
-
-      return swrResponse.mutate({ openedQuestionnaireId: null });
-    },
-  };
-};

+ 0 - 24
apps/app/src/features/questionnaire/client/stores/questionnaire.tsx

@@ -1,24 +0,0 @@
-import type { SWRResponse } from 'swr';
-import useSWR from 'swr';
-
-import { apiv3Get } from '~/client/util/apiv3-client';
-
-import type { IQuestionnaireOrderHasId } from '../../interfaces/questionnaire-order';
-
-export const useSWRxQuestionnaireOrders = (): SWRResponse<IQuestionnaireOrderHasId[], Error> => {
-  return useSWR(
-    '/questionnaire/orders',
-    endpoint => apiv3Get(endpoint).then((response) => {
-      return response.data.questionnaireOrders;
-    }),
-  );
-};
-
-export const useSWRxIsQuestionnaireEnabled = (): SWRResponse<boolean, Error> => {
-  return useSWR(
-    '/questionnaire/is-enabled',
-    endpoint => apiv3Get(endpoint)
-      .then(response => !!response.data.isEnabled)
-      .catch(() => false),
-  );
-};

+ 0 - 4
apps/app/src/features/questionnaire/interfaces/answer.ts

@@ -1,4 +0,0 @@
-export interface IAnswer<ID = string> {
-  question: ID
-  value: string
-}

+ 0 - 25
apps/app/src/features/questionnaire/interfaces/condition.ts

@@ -1,25 +0,0 @@
-import type { HasObjectId } from '@growi/core';
-import type { GrowiServiceType } from '@growi/core/dist/consts';
-
-import type { UserType } from './user-info';
-
-
-interface UserCondition {
-  types: UserType[] // user types to show questionnaire
-  daysSinceCreation?: {
-    moreThanOrEqualTo?: number
-    lessThanOrEqualTo?: number
-  }
-}
-
-interface GrowiCondition {
-  types: GrowiServiceType[] // GROWI types to show questionnaire in
-  versionRegExps: string[] // GROWI versions to show questionnaire in
-}
-
-export interface ICondition {
-  user: UserCondition
-  growi: GrowiCondition
-}
-
-export type IConditionHasId = ICondition & HasObjectId;

+ 0 - 27
apps/app/src/features/questionnaire/interfaces/proactive-questionnaire-answer.ts

@@ -1,27 +0,0 @@
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from './growi-app-info';
-import type { IUserInfo } from './user-info';
-
-
-export interface IProactiveQuestionnaireAnswer {
-  satisfaction: number,
-  commentText: string,
-  growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>,
-  userInfo: IUserInfo,
-  answeredAt: Date,
-  lengthOfExperience?: string,
-  position?: string,
-  occupation?: string,
-}
-
-export interface IProactiveQuestionnaireAnswerLegacy {
-  satisfaction: number,
-  commentText: string,
-  growiInfo: IGrowiAppInfoLegacy,
-  userInfo: IUserInfo,
-  answeredAt: Date,
-  lengthOfExperience?: string,
-  position?: string,
-  occupation?: string,
-}

+ 0 - 15
apps/app/src/features/questionnaire/interfaces/question.ts

@@ -1,15 +0,0 @@
-import type { HasObjectId } from '@growi/core';
-
-export const QuestionType = { points: 'points', text: 'text' } as const;
-
-type QuestionType = typeof QuestionType[keyof typeof QuestionType];
-
-export interface IQuestion {
-  type: QuestionType
-  text: {
-    ja_JP: string
-    en_US: string
-  }
-}
-
-export type IQuestionHasId = IQuestion & HasObjectId;

+ 0 - 16
apps/app/src/features/questionnaire/interfaces/questionnaire-answer-status.ts

@@ -1,16 +0,0 @@
-// eslint-disable-next-line max-len
-// see: https://dev.growi.org/6385911e1632aa30f4dae6a4#mdcont-%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%94%E3%81%A8%E3%81%AB%E5%9B%9E%E7%AD%94%E6%B8%88%E3%81%BF%E3%81%8B%E3%81%A9%E3%81%86%E3%81%8B%E3%82%92%E4%BF%9D%E5%AD%98%E3%81%99%E3%82%8B
-
-import type { Types } from 'mongoose';
-
-export const StatusType = {
-  not_answered: 'not_answered', answered: 'answered', skipped: 'skipped', denied: 'denied',
-} as const;
-
-export type StatusType = typeof StatusType[keyof typeof StatusType];
-
-export interface IQuestionnaireAnswerStatus {
-  user: Types.ObjectId | string // user that answered questionnaire
-  questionnaireOrderId: string
-  status: StatusType
-}

+ 0 - 21
apps/app/src/features/questionnaire/interfaces/questionnaire-answer.ts

@@ -1,21 +0,0 @@
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type { IAnswer } from './answer';
-import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from './growi-app-info';
-import type { IUserInfo } from './user-info';
-
-export interface IQuestionnaireAnswer<ID = string> {
-  answers: IAnswer[]
-  answeredAt: Date
-  growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>
-  userInfo: IUserInfo
-  questionnaireOrder: ID
-}
-
-export interface IQuestionnaireAnswerLegacy<ID = string> {
-  answers: IAnswer[]
-  answeredAt: Date
-  growiInfo: IGrowiAppInfoLegacy,
-  userInfo: IUserInfo
-  questionnaireOrder: ID
-}

+ 0 - 21
apps/app/src/features/questionnaire/interfaces/questionnaire-order.ts

@@ -1,21 +0,0 @@
-import type { HasObjectId } from '@growi/core';
-
-import type { ICondition, IConditionHasId } from './condition';
-import type { IQuestion, IQuestionHasId } from './question';
-
-export interface IQuestionnaireOrder<TQUESTION = IQuestion, TCONDITION = ICondition> {
-  shortTitle: {
-    ja_JP: string
-    en_US: string
-  }
-  title: {
-    ja_JP: string
-    en_US: string
-  }
-  showFrom: Date
-  showUntil: Date
-  questions: TQUESTION[]
-  condition: TCONDITION
-}
-
-export type IQuestionnaireOrderHasId = IQuestionnaireOrder<IQuestionHasId, IConditionHasId> & HasObjectId;

+ 0 - 12
apps/app/src/features/questionnaire/interfaces/user-info.ts

@@ -1,12 +0,0 @@
-export const UserType = { admin: 'admin', general: 'general', guest: 'guest' } as const;
-
-type guestType = typeof UserType.guest
-export type UserType = typeof UserType[keyof typeof UserType];
-
-export type IUserInfo = {
-  userIdHash: string // userId hash generated by using appSiteUrl as salt
-  type: Exclude<UserType, guestType>
-  userCreatedAt: Date // createdAt of user that answered the questionnaire
-} | {
-  type: guestType
-}

+ 0 - 28
apps/app/src/features/questionnaire/server/models/proactive-questionnaire-answer.ts

@@ -1,28 +0,0 @@
-import type { Model } from 'mongoose';
-import { Schema } from 'mongoose';
-
-import { getOrCreateModel } from '~/server/util/mongoose-utils';
-
-import type { IProactiveQuestionnaireAnswer } from '../../interfaces/proactive-questionnaire-answer';
-
-import { growiInfoSchema } from './schema/growi-info';
-import { userInfoSchema } from './schema/user-info';
-
-interface ProactiveQuestionnaireAnswerDocument extends IProactiveQuestionnaireAnswer, Document {}
-
-type ProactiveQuestionnaireAnswerModel = Model<ProactiveQuestionnaireAnswerDocument>
-
-export const proactiveQuestionnaireAnswerSchema = new Schema<ProactiveQuestionnaireAnswerDocument>({
-  satisfaction: { type: Number, required: true },
-  lengthOfExperience: { type: String },
-  position: { type: String },
-  occupation: { type: String },
-  commentText: { type: String, required: true },
-  growiInfo: { type: growiInfoSchema, required: true },
-  userInfo: { type: userInfoSchema, required: true },
-  answeredAt: { type: Date },
-}, { timestamps: true });
-
-export default getOrCreateModel<ProactiveQuestionnaireAnswerDocument, ProactiveQuestionnaireAnswerModel>(
-  'ProactiveQuestionnaireAnswer', proactiveQuestionnaireAnswerSchema,
-);

+ 0 - 19
apps/app/src/features/questionnaire/server/models/questionnaire-answer-status.ts

@@ -1,19 +0,0 @@
-import type { Model, Document } from 'mongoose';
-import { Schema } from 'mongoose';
-
-import { getOrCreateModel } from '~/server/util/mongoose-utils';
-
-import type { IQuestionnaireAnswerStatus } from '../../interfaces/questionnaire-answer-status';
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-
-export interface QuestionnaireAnswerStatusDocument extends IQuestionnaireAnswerStatus, Document {}
-
-export type QuestionnaireAnswerStatusModel = Model<QuestionnaireAnswerStatusDocument>
-
-const questionnaireOrderSchema = new Schema<QuestionnaireAnswerStatusDocument>({
-  user: { type: Schema.Types.ObjectId, required: true },
-  questionnaireOrderId: { type: String, required: true },
-  status: { type: String, enum: Object.values(StatusType), default: StatusType.not_answered },
-}, { timestamps: true });
-
-export default getOrCreateModel<QuestionnaireAnswerStatusDocument, QuestionnaireAnswerStatusModel>('QuestionnaireAnswerStatus', questionnaireOrderSchema);

+ 0 - 25
apps/app/src/features/questionnaire/server/models/questionnaire-answer.ts

@@ -1,25 +0,0 @@
-import type { Document, Model } from 'mongoose';
-import { Schema } from 'mongoose';
-
-import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-import { getOrCreateModel } from '~/server/util/mongoose-utils';
-
-import type { IQuestionnaireAnswer } from '../../interfaces/questionnaire-answer';
-
-import { answerSchema } from './schema/answer';
-import { growiInfoSchema } from './schema/growi-info';
-import { userInfoSchema } from './schema/user-info';
-
-interface QuestionnaireAnswerDocument extends IQuestionnaireAnswer<ObjectIdLike>, Document {}
-
-type QuestionnaireAnswerModel = Model<QuestionnaireAnswerDocument>
-
-const questionnaireAnswerSchema = new Schema<QuestionnaireAnswerDocument>({
-  answers: [answerSchema],
-  answeredAt: { type: Date, required: true },
-  growiInfo: { type: growiInfoSchema, required: true },
-  userInfo: { type: userInfoSchema, required: true },
-  questionnaireOrder: { type: Schema.Types.ObjectId, ref: 'QuestionnaireOrder' },
-}, { timestamps: true });
-
-export default getOrCreateModel<QuestionnaireAnswerDocument, QuestionnaireAnswerModel>('QuestionnaireAnswer', questionnaireAnswerSchema);

+ 0 - 35
apps/app/src/features/questionnaire/server/models/questionnaire-order.ts

@@ -1,35 +0,0 @@
-import type { Model, Document } from 'mongoose';
-import { Schema } from 'mongoose';
-
-import { getOrCreateModel } from '~/server/util/mongoose-utils';
-
-import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
-
-import conditionSchema from './schema/condition';
-import questionSchema from './schema/question';
-
-export interface QuestionnaireOrderDocument extends IQuestionnaireOrder, Document {}
-
-export type QuestionnaireOrderModel = Model<QuestionnaireOrderDocument>
-
-const questionnaireOrderTitleSchema = new Schema<IQuestionnaireOrder['title']>({
-  ja_JP: { type: String, required: true },
-  en_US: { type: String, required: true },
-}, { _id: false });
-
-const questionnaireOrderSchema = new Schema<QuestionnaireOrderDocument>({
-  shortTitle: { type: questionnaireOrderTitleSchema, required: true },
-  title: { type: questionnaireOrderTitleSchema, required: true },
-  showFrom: { type: Date, required: true },
-  showUntil: {
-    type: Date,
-    required: true,
-    validate: [function(value) {
-      return this.showFrom <= value;
-    }, 'showFrom must be before showUntil'],
-  },
-  questions: [questionSchema],
-  condition: { type: conditionSchema, required: true },
-}, { timestamps: true });
-
-export default getOrCreateModel<QuestionnaireOrderDocument, QuestionnaireOrderModel>('QuestionnaireOrder', questionnaireOrderSchema);

+ 0 - 10
apps/app/src/features/questionnaire/server/models/schema/answer.ts

@@ -1,10 +0,0 @@
-import { Schema } from 'mongoose';
-
-import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-
-import type { IAnswer } from '../../../interfaces/answer';
-
-export const answerSchema = new Schema<IAnswer<ObjectIdLike>>({
-  question: { type: Schema.Types.ObjectId, ref: 'Question', required: true },
-  value: { type: String, required: true },
-});

+ 0 - 29
apps/app/src/features/questionnaire/server/models/schema/condition.ts

@@ -1,29 +0,0 @@
-import { GrowiServiceType } from '@growi/core/dist/consts';
-import { Schema } from 'mongoose';
-
-import type { ICondition } from '../../../interfaces/condition';
-import { UserType } from '../../../interfaces/user-info';
-
-const conditionSchema = new Schema<ICondition>({
-  user: {
-    types: [{ type: String, enum: Object.values(UserType) }],
-    daysSinceCreation: {
-      moreThanOrEqualTo: { type: Number, min: 0 },
-      lessThanOrEqualTo: {
-        type: Number,
-        min: 0,
-        validate: [
-          function(value) {
-            return this.user.daysSinceCreation.moreThanOrEqualTo == null || this.user.daysSinceCreation.moreThanOrEqualTo <= value;
-          }, 'daysSinceCreation.lessThanOrEqualTo must be greater than moreThanOrEqualTo',
-        ],
-      },
-    },
-  },
-  growi: {
-    types: [{ type: String, enum: Object.values(GrowiServiceType) }],
-    versionRegExps: [String],
-  },
-});
-
-export default conditionSchema;

+ 0 - 42
apps/app/src/features/questionnaire/server/models/schema/growi-info.ts

@@ -1,42 +0,0 @@
-import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-import { GrowiWikiType } from '@growi/core/dist/interfaces';
-import { Schema } from 'mongoose';
-
-import type { IGrowiAppAdditionalInfo } from '~/features/questionnaire/interfaces/growi-app-info';
-import { AttachmentMethodType } from '~/interfaces/attachment';
-import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
-
-const growiAdditionalInfoSchema = new Schema<IGrowiAppAdditionalInfo>({
-  installedAt: { type: Date, required: true },
-  installedAtByOldestUser: { type: Date, required: true },
-  currentUsersCount: { type: Number, required: true },
-  currentActiveUsersCount: { type: Number, required: true },
-  attachmentType: { type: String, required: true, enum: Object.values(AttachmentMethodType) },
-  activeExternalAccountTypes: [{ type: String, enum: Object.values(IExternalAuthProviderType) }],
-});
-
-export const growiInfoSchema = new Schema<IGrowiInfo<IGrowiAppAdditionalInfo> & IGrowiAppAdditionalInfo>({
-  version: { type: String, required: true },
-  appSiteUrl: { type: String },
-  serviceInstanceId: { type: String, required: true },
-  type: { type: String, required: true, enum: Object.values(GrowiServiceType) },
-  wikiType: { type: String, required: true, enum: Object.values(GrowiWikiType) },
-  osInfo: {
-    type: { type: String },
-    platform: String,
-    arch: String,
-    totalmem: Number,
-  },
-  deploymentType: { type: String, enum: (<(string | null)[]>Object.values(GrowiDeploymentType)).concat([null]) },
-  additionalInfo: growiAdditionalInfoSchema,
-
-  // legacy properties (extracted from additionalInfo for growi-questionnaire)
-  // see: https://gitlab.weseek.co.jp/tech/growi/growi-questionnaire
-  installedAt: { type: Date },
-  installedAtByOldestUser: { type: Date },
-  currentUsersCount: { type: Number },
-  currentActiveUsersCount: { type: Number },
-  attachmentType: { type: String, enum: Object.values(AttachmentMethodType) },
-  activeExternalAccountTypes: [{ type: String, enum: Object.values(IExternalAuthProviderType) }],
-});

+ 0 - 16
apps/app/src/features/questionnaire/server/models/schema/question.ts

@@ -1,16 +0,0 @@
-import { Schema } from 'mongoose';
-
-import type { IQuestion } from '../../../interfaces/question';
-import { QuestionType } from '../../../interfaces/question';
-
-const questionTextSchema = new Schema<IQuestion['text']>({
-  ja_JP: { type: String, required: true },
-  en_US: { type: String, required: true },
-}, { _id: false });
-
-const questionSchema = new Schema<IQuestion>({
-  type: { type: String, required: true, enum: Object.values(QuestionType) },
-  text: { type: questionTextSchema, required: true },
-}, { timestamps: true });
-
-export default questionSchema;

+ 0 - 10
apps/app/src/features/questionnaire/server/models/schema/user-info.ts

@@ -1,10 +0,0 @@
-import { Schema } from 'mongoose';
-
-import type { IUserInfo } from '../../../interfaces/user-info';
-import { UserType } from '../../../interfaces/user-info';
-
-export const userInfoSchema = new Schema<IUserInfo>({
-  userIdHash: { type: String },
-  type: { type: String, required: true, enum: Object.values(UserType) },
-  userCreatedAt: { type: Date },
-});

+ 0 - 384
apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts

@@ -1,384 +0,0 @@
-import type { IUserHasId } from '@growi/core';
-import type { Request } from 'express';
-import { Router } from 'express';
-import { body, validationResult } from 'express-validator';
-
-import type Crowi from '~/server/crowi';
-import { accessTokenParser } from '~/server/middlewares/access-token-parser';
-import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
-import { configManager } from '~/server/service/config-manager';
-import { growiInfoService } from '~/server/service/growi-info';
-import axios from '~/utils/axios';
-import loggerFactory from '~/utils/logger';
-
-import type { IAnswer } from '../../../interfaces/answer';
-import type { IProactiveQuestionnaireAnswer } from '../../../interfaces/proactive-questionnaire-answer';
-import type { IQuestionnaireAnswer } from '../../../interfaces/questionnaire-answer';
-import { StatusType } from '../../../interfaces/questionnaire-answer-status';
-import ProactiveQuestionnaireAnswer from '../../models/proactive-questionnaire-answer';
-import QuestionnaireAnswer from '../../models/questionnaire-answer';
-import QuestionnaireAnswerStatus from '../../models/questionnaire-answer-status';
-import { convertToLegacyFormat, getSiteUrlHashed } from '../../util/convert-to-legacy-format';
-
-
-const logger = loggerFactory('growi:routes:apiv3:questionnaire');
-
-const router = Router();
-
-interface AuthorizedRequest extends Request {
-  user?: any
-}
-
-module.exports = (crowi: Crowi): Router => {
-  const loginRequired = require('~/server/middlewares/login-required')(crowi, true);
-
-  const validators = {
-    proactiveAnswer: [
-      body('satisfaction').exists().isNumeric(),
-      body('lengthOfExperience').isString(),
-      body('position').isString(),
-      body('occupation').isString(),
-      body('commentText').exists().isString(),
-    ],
-    answer: [body('questionnaireOrderId').exists().isString(), body('answers').exists().isArray({ min: 1 })],
-    skipDeny: [body('questionnaireOrderId').exists().isString()],
-  };
-
-  const changeAnswerStatus = async(user, questionnaireOrderId, status) => {
-    const result = await QuestionnaireAnswerStatus.updateOne({
-      user: { $eq: user },
-      questionnaireOrderId: { $eq: questionnaireOrderId },
-    }, {
-      status,
-    }, { upsert: true });
-
-    if (result.modifiedCount === 1) {
-      return 204;
-    }
-    if (result.upsertedCount === 1) {
-      return 201;
-    }
-    return 404;
-  };
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/orders:
-   *   get:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/orders
-   *     description: Get questionnaire orders
-   *     responses:
-   *       200:
-   *         description: OK
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *               properties:
-   *                 questionnaireOrders:
-   *                   type: array
-   *                   items:
-   *                     type: object
-   */
-  router.get('/orders', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const growiInfo = await growiInfoService.getGrowiInfo(true);
-    const userInfo = crowi.questionnaireService.getUserInfo(req.user ?? null, getSiteUrlHashed(growiInfo.appSiteUrl));
-
-    try {
-      const questionnaireOrders = await crowi.questionnaireService!.getQuestionnaireOrdersToShow(userInfo, growiInfo, req.user?._id ?? null);
-
-      return res.apiv3({ questionnaireOrders });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/is-enabled:
-   *   get:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/is-enabled
-   *     description: Get questionnaire is enabled
-   *     responses:
-   *       200:
-   *         description: OK
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *               properties:
-   *                 isEnabled:
-   *                   type: boolean
-   */
-  router.get('/is-enabled', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const isEnabled = configManager.getConfig('questionnaire:isQuestionnaireEnabled');
-    return res.apiv3({ isEnabled });
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/proactive/answer:
-   *   post:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/proactive/answer
-   *     description: Post proactive questionnaire answer
-   *     requestBody:
-   *       required: true
-   *       content:
-   *         application/json:
-   *           schema:
-   *             type: object
-   *     responses:
-   *       200:
-   *         description: Success
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   */
-  router.post('/proactive/answer', accessTokenParser, loginRequired, validators.proactiveAnswer, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const sendQuestionnaireAnswer = async() => {
-      const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
-      const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
-      const growiInfo = await growiInfoService.getGrowiInfo(true);
-      const userInfo = crowi.questionnaireService.getUserInfo(req.user ?? null, getSiteUrlHashed(growiInfo.appSiteUrl));
-
-      const proactiveQuestionnaireAnswer: IProactiveQuestionnaireAnswer = {
-        satisfaction: req.body.satisfaction,
-        lengthOfExperience: req.body.lengthOfExperience,
-        position: req.body.position,
-        occupation: req.body.occupation,
-        commentText: req.body.commentText,
-        growiInfo,
-        userInfo,
-        answeredAt: new Date(),
-      };
-
-      const proactiveQuestionnaireAnswerLegacy = convertToLegacyFormat(proactiveQuestionnaireAnswer, isAppSiteUrlHashed);
-
-      try {
-        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive`, proactiveQuestionnaireAnswerLegacy);
-      }
-      catch (err) {
-        if (err.request != null) {
-          // when failed to send, save to resend in cronjob
-          await ProactiveQuestionnaireAnswer.create(proactiveQuestionnaireAnswer);
-        }
-        else {
-          throw err;
-        }
-      }
-    };
-
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    try {
-      await sendQuestionnaireAnswer();
-      return res.apiv3({});
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/answer:
-   *   put:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/answer
-   *     description: Post questionnaire answer
-   *     requestBody:
-   *       required: true
-   *       content:
-   *         application/json:
-   *           schema:
-   *             type: object
-   *     responses:
-   *       201:
-   *         description: Created
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       204:
-   *         description: No Content
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       404:
-   *         description: Not Found
-   */
-  router.put('/answer', accessTokenParser, loginRequired, validators.answer, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const sendQuestionnaireAnswer = async(user: IUserHasId, answers: IAnswer[]) => {
-      const questionnaireServerOrigin = crowi.configManager.getConfig('app:questionnaireServerOrigin');
-      const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
-      const growiInfo = await growiInfoService.getGrowiInfo(true);
-      const userInfo = crowi.questionnaireService.getUserInfo(user, getSiteUrlHashed(growiInfo.appSiteUrl));
-
-      const questionnaireAnswer: IQuestionnaireAnswer = {
-        growiInfo,
-        userInfo,
-        answers,
-        answeredAt: new Date(),
-        questionnaireOrder: req.body.questionnaireOrderId,
-      };
-
-      const questionnaireAnswerLegacy = convertToLegacyFormat(questionnaireAnswer, isAppSiteUrlHashed);
-
-      try {
-        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer`, questionnaireAnswerLegacy);
-      }
-      catch (err) {
-        if (err.request != null) {
-          // when failed to send, save to resend in cronjob
-          await QuestionnaireAnswer.create(questionnaireAnswer);
-        }
-        else {
-          throw err;
-        }
-      }
-    };
-
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    try {
-      await sendQuestionnaireAnswer(req.user ?? null, req.body.answers);
-      const status = await changeAnswerStatus(req.user, req.body.questionnaireOrderId, StatusType.answered);
-      return res.apiv3({}, status);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/skip:
-   *   put:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/skip
-   *     description: Skip questionnaire
-   *     requestBody:
-   *       required: true
-   *       content:
-   *         application/json:
-   *           schema:
-   *             type: object
-   *     responses:
-   *       201:
-   *         description: Created
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       204:
-   *         description: No Content
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       404:
-   *         description: Not Found
-   */
-  router.put('/skip', accessTokenParser, loginRequired, validators.skipDeny, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    try {
-      const status = await changeAnswerStatus(req.user, req.body.questionnaireOrderId, StatusType.skipped);
-      return res.apiv3({}, status);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/deny:
-   *   put:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/deny
-   *     description: Deny questionnaire
-   *     requestBody:
-   *       required: true
-   *       content:
-   *         application/json:
-   *           schema:
-   *             type: object
-   *     responses:
-   *       201:
-   *         description: Created
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       204:
-   *         description: No Content
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       404:
-   *         description: Not Found
-   */
-  router.put('/deny', accessTokenParser, loginRequired, validators.skipDeny, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    try {
-      const status = await changeAnswerStatus(req.user, req.body.questionnaireOrderId, StatusType.denied);
-      return res.apiv3({}, status);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  return router;
-
-};

+ 0 - 515
apps/app/src/features/questionnaire/server/service/questionnaire-cron.integ.ts

@@ -1,515 +0,0 @@
-import { GrowiDeploymentType, GrowiServiceType, GrowiWikiType } from '@growi/core';
-// eslint-disable-next-line no-restricted-imports
-import axios from 'axios';
-import mongoose from 'mongoose';
-
-import { configManager } from '~/server/service/config-manager';
-
-import { AttachmentMethodType } from '../../../../interfaces/attachment';
-import type {
-  IProactiveQuestionnaireAnswer, IProactiveQuestionnaireAnswerLegacy,
-} from '../../interfaces/proactive-questionnaire-answer';
-import type { IQuestionnaireAnswer, IQuestionnaireAnswerLegacy } from '../../interfaces/questionnaire-answer';
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import ProactiveQuestionnaireAnswer from '../models/proactive-questionnaire-answer';
-import QuestionnaireAnswer from '../models/questionnaire-answer';
-import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import QuestionnaireOrder from '../models/questionnaire-order';
-
-import questionnaireCronService from './questionnaire-cron';
-
-// TODO: use actual user model after ~/server/models/user.js becomes importable in vitest
-// ref: https://github.com/vitest-dev/vitest/issues/846
-const userSchema = new mongoose.Schema({
-  name: { type: String },
-  username: { type: String, required: true, unique: true },
-  email: { type: String, unique: true, sparse: true },
-}, {
-  timestamps: true,
-});
-const User = mongoose.model('User', userSchema);
-
-describe('QuestionnaireCronService', () => {
-  const mockResponse = {
-    data: {
-      questionnaireOrders: [
-        // saved in db、not finished (user.types is updated from the time it was saved)
-        {
-          _id: '63a8354837e7aa378e16f0b1',
-          shortTitle: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          title: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          showFrom: '2022-12-11',
-          showUntil: '2100-12-12',
-          questions: [
-            {
-              type: 'points',
-              text: {
-                ja_JP: 'GROWI は使いやすいですか?',
-                en_US: 'Is GROWI easy to use?',
-              },
-            },
-          ],
-          condition: {
-            user: {
-              types: ['admin', 'general'],
-            },
-            growi: {
-              types: ['cloud', 'private-cloud'],
-              versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-            },
-          },
-          createdAt: '2022-12-01',
-          updatedAt: '2022-12-01',
-          __v: 0,
-        },
-        // not saved, not finished
-        {
-          _id: '63a8354837e7aa378e16f0b2',
-          shortTitle: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          title: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          showFrom: '2021-12-11',
-          showUntil: '2100-12-12',
-          questions: [
-            {
-              type: 'points',
-              text: {
-                ja_JP: 'アンケート機能は正常動作していますか?',
-                en_US: 'Is this questionnaire functioning properly?',
-              },
-            },
-          ],
-          condition: {
-            user: {
-              types: ['general'],
-            },
-            growi: {
-              types: ['cloud'],
-              versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-            },
-          },
-          createdAt: '2022-12-02',
-          updatedAt: '2022-12-02',
-          __v: 0,
-        },
-        // not saved, finished
-        {
-          _id: '63a8354837e7aa378e16f0b3',
-          shortTitle: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          title: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          showFrom: '2021-12-11',
-          showUntil: '2021-12-12',
-          questions: [
-            {
-              type: 'points',
-              text: {
-                ja_JP: 'これはいい質問ですか?',
-                en_US: 'Is this a good question?',
-              },
-            },
-          ],
-          condition: {
-            user: {
-              types: ['general'],
-            },
-            growi: {
-              types: ['cloud'],
-              versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-            },
-          },
-          createdAt: '2022-12-03',
-          updatedAt: '2022-12-03',
-          __v: 0,
-        },
-      ],
-    },
-  };
-
-  beforeAll(async() => {
-    await configManager.loadConfigs();
-    await configManager.updateConfig('app:questionnaireCronMaxHoursUntilRequest', 0);
-    await User.create({
-      name: 'Example for Questionnaire Service Test',
-      username: 'questionnaire cron test user',
-      email: 'questionnaireCronTestUser@example.com',
-      createdAt: '2020-01-01',
-    });
-  });
-
-  beforeEach(async() => {
-    // insert initial db data
-    await QuestionnaireOrder.insertMany([
-      {
-        _id: '63a8354837e7aa378e16f0b1',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2022-12-11',
-        showUntil: '2100-12-12',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: 'GROWI は使いやすいですか?',
-              en_US: 'Is GROWI easy to use?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['general'],
-          },
-          growi: {
-            types: ['cloud', 'private-cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-      },
-      // finished
-      {
-        _id: '63a8354837e7aa378e16f0b4',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2020-12-11',
-        showUntil: '2021-12-12',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: 'ver 2.0 は 1.0 より良いですか?',
-              en_US: 'Is ver 2.0 better than 1.0?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['general'],
-          },
-          growi: {
-            types: ['cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-      },
-      // questionnaire that doesn't exist in questionnaire server
-      {
-        _id: '63a8354837e7aa378e16f0b5',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2020-12-11',
-        showUntil: '2100-12-12',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: '新しいデザインは良いですか?',
-              en_US: 'How would you rate the latest design?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['general'],
-          },
-          growi: {
-            types: ['cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-      },
-    ]);
-
-    await QuestionnaireAnswerStatus.insertMany([
-      {
-        user: new mongoose.Types.ObjectId(),
-        questionnaireOrderId: '63a8354837e7aa378e16f0b1',
-        status: StatusType.skipped,
-      },
-      {
-        user: new mongoose.Types.ObjectId(),
-        questionnaireOrderId: '63a8354837e7aa378e16f0b1',
-        status: StatusType.answered,
-      },
-      {
-        user: new mongoose.Types.ObjectId(),
-        questionnaireOrderId: '63a8354837e7aa378e16f0b1',
-        status: StatusType.not_answered,
-      },
-    ]);
-
-    const validQuestionnaireAnswer: IQuestionnaireAnswer = {
-      answers: [{
-        question: '63c6da88143e531d95346188',
-        value: '1',
-      }],
-      answeredAt: new Date(),
-      growiInfo: {
-        version: '1.0',
-        appSiteUrl: 'https://example.com',
-        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
-        type: GrowiServiceType.cloud,
-        wikiType: GrowiWikiType.open,
-        deploymentType: GrowiDeploymentType.others,
-        osInfo: {
-          type: 'Linux',
-          platform: 'linux',
-          arch: 'x64',
-          totalmem: 8589934592,
-        },
-        additionalInfo: {
-          installedAt: new Date('2000-01-01'),
-          installedAtByOldestUser: new Date('2020-01-01'),
-          currentUsersCount: 100,
-          currentActiveUsersCount: 50,
-          attachmentType: AttachmentMethodType.aws,
-        },
-      },
-      userInfo: {
-        userIdHash: '542bcc3bc5bc61b840017a18',
-        type: 'general',
-        userCreatedAt: new Date(),
-      },
-      questionnaireOrder: '63a8354837e7aa378e16f0b1',
-    };
-
-    const validQuestionnaireAnswerLegacy: IQuestionnaireAnswerLegacy = {
-      answers: [{
-        question: '63c6da88143e531d95346188',
-        value: '1',
-      }],
-      answeredAt: new Date(),
-      growiInfo: {
-        version: '1.0',
-        appSiteUrl: 'https://example.com',
-        appSiteUrlHashed: 'hashed',
-        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
-        type: GrowiServiceType.cloud,
-        wikiType: GrowiWikiType.open,
-        deploymentType: GrowiDeploymentType.others,
-        installedAt: new Date('2000-01-01'),
-        installedAtByOldestUser: new Date('2020-01-01'),
-        currentUsersCount: 100,
-        currentActiveUsersCount: 50,
-        osInfo: {
-          type: 'Linux',
-          platform: 'linux',
-          arch: 'x64',
-          totalmem: 8589934592,
-        },
-        attachmentType: AttachmentMethodType.aws,
-      },
-      userInfo: {
-        userIdHash: '542bcc3bc5bc61b840017a18',
-        type: 'general',
-        userCreatedAt: new Date(),
-      },
-      questionnaireOrder: '63a8354837e7aa378e16f0b1',
-    };
-
-    await QuestionnaireAnswer.insertMany([
-      validQuestionnaireAnswer,
-      validQuestionnaireAnswer,
-      validQuestionnaireAnswer,
-      validQuestionnaireAnswerLegacy,
-      validQuestionnaireAnswerLegacy,
-    ]);
-
-    const validProactiveQuestionnaireAnswer: IProactiveQuestionnaireAnswer = {
-      satisfaction: 1,
-      commentText: 'answer text',
-      growiInfo: {
-        version: '1.0',
-        appSiteUrl: 'https://example.com',
-        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
-        type: GrowiServiceType.cloud,
-        wikiType: GrowiWikiType.open,
-        deploymentType: GrowiDeploymentType.others,
-        osInfo: {
-          type: 'Linux',
-          platform: 'linux',
-          arch: 'x64',
-          totalmem: 8589934592,
-        },
-        additionalInfo: {
-          installedAt: new Date('2000-01-01'),
-          installedAtByOldestUser: new Date('2020-01-01'),
-          currentUsersCount: 100,
-          currentActiveUsersCount: 50,
-          attachmentType: AttachmentMethodType.aws,
-        },
-      },
-      userInfo: {
-        userIdHash: '542bcc3bc5bc61b840017a18',
-        type: 'general',
-        userCreatedAt: new Date(),
-      },
-      answeredAt: new Date(),
-    };
-    const validProactiveQuestionnaireAnswerLegacy: IProactiveQuestionnaireAnswerLegacy = {
-      satisfaction: 1,
-      commentText: 'answer text',
-      growiInfo: {
-        version: '1.0',
-        appSiteUrl: 'https://example.com',
-        appSiteUrlHashed: 'hashed',
-        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
-        type: GrowiServiceType.cloud,
-        wikiType: GrowiWikiType.open,
-        deploymentType: GrowiDeploymentType.others,
-        osInfo: {
-          type: 'Linux',
-          platform: 'linux',
-          arch: 'x64',
-          totalmem: 8589934592,
-        },
-        // legacy properties
-        installedAt: new Date('2000-01-01'),
-        installedAtByOldestUser: new Date('2020-01-01'),
-        currentUsersCount: 100,
-        currentActiveUsersCount: 50,
-        attachmentType: AttachmentMethodType.aws,
-      },
-      userInfo: {
-        userIdHash: '542bcc3bc5bc61b840017a18',
-        type: 'general',
-        userCreatedAt: new Date(),
-      },
-      answeredAt: new Date(),
-    };
-
-    await ProactiveQuestionnaireAnswer.insertMany([
-      validProactiveQuestionnaireAnswer,
-      validProactiveQuestionnaireAnswer,
-      validProactiveQuestionnaireAnswer,
-      validProactiveQuestionnaireAnswerLegacy,
-      validProactiveQuestionnaireAnswerLegacy,
-    ]);
-
-    questionnaireCronService.startCron();
-
-    vi.spyOn(axios, 'get').mockResolvedValue(mockResponse);
-    vi.spyOn(axios, 'post').mockResolvedValue({ data: { result: 'success' } });
-  });
-
-  afterAll(() => {
-    questionnaireCronService.stopCron(); // vitest will not finish until cronjob stops
-  });
-
-  test('Job execution should save(update) quesionnaire orders, delete outdated ones, update skipped answer statuses, and delete resent answers', async() => {
-    // testing the cronjob from schedule has untrivial overhead, so test job execution in place
-    await questionnaireCronService.executeJob();
-
-    const savedOrders = await QuestionnaireOrder.find()
-      .select('-condition._id -questions._id -questions.createdAt -questions.updatedAt')
-      .sort({ _id: 1 });
-
-    expect(JSON.parse(JSON.stringify(savedOrders))).toEqual([
-      {
-        _id: '63a8354837e7aa378e16f0b1',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2022-12-11T00:00:00.000Z',
-        showUntil: '2100-12-12T00:00:00.000Z',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: 'GROWI は使いやすいですか?',
-              en_US: 'Is GROWI easy to use?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['admin', 'general'],
-          },
-          growi: {
-            types: ['cloud', 'private-cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-        createdAt: '2022-12-01T00:00:00.000Z',
-        updatedAt: '2022-12-01T00:00:00.000Z',
-        __v: 0,
-      },
-      {
-        _id: '63a8354837e7aa378e16f0b2',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2021-12-11T00:00:00.000Z',
-        showUntil: '2100-12-12T00:00:00.000Z',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: 'アンケート機能は正常動作していますか?',
-              en_US: 'Is this questionnaire functioning properly?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['general'],
-          },
-          growi: {
-            types: ['cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-        createdAt: '2022-12-02T00:00:00.000Z',
-        updatedAt: '2022-12-02T00:00:00.000Z',
-        __v: 0,
-      },
-    ]);
-
-    expect((await QuestionnaireAnswerStatus.find({ status: StatusType.not_answered })).length).toEqual(2);
-    expect((await QuestionnaireAnswer.find()).length).toEqual(0);
-    expect((await ProactiveQuestionnaireAnswer.find()).length).toEqual(0);
-  });
-});

+ 0 - 106
apps/app/src/features/questionnaire/server/service/questionnaire-cron.ts

@@ -1,106 +0,0 @@
-import axiosRetry from 'axios-retry';
-
-import { configManager } from '~/server/service/config-manager';
-import CronService from '~/server/service/cron';
-import { getRandomIntInRange } from '~/utils/rand';
-
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
-import ProactiveQuestionnaireAnswer from '../models/proactive-questionnaire-answer';
-import QuestionnaireAnswer from '../models/questionnaire-answer';
-import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import QuestionnaireOrder from '../models/questionnaire-order';
-import { convertToLegacyFormat } from '../util/convert-to-legacy-format';
-
-const axios = require('axios').default;
-
-axiosRetry(axios, { retries: 3 });
-
-/**
- * Manages cronjob which
- *  1. fetches QuestionnaireOrders from questionnaire server
- *  2. updates QuestionnaireOrder collection to contain only the ones that exist in the fetched list and is not finished (doesn't have to be started)
- *  3. changes QuestionnaireAnswerStatuses which are 'skipped' to 'not_answered'
- *  4. resend QuestionnaireAnswers & ProactiveQuestionnaireAnswers which failed to reach questionnaire server
- */
-class QuestionnaireCronService extends CronService {
-
-  sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));
-
-  override getCronSchedule(): string {
-    return configManager.getConfig('app:questionnaireCronSchedule');
-  }
-
-  override async executeJob(): Promise<void> {
-    // sleep for a random amount to scatter request time from GROWI apps to questionnaire server
-    await this.sleepBeforeJob();
-
-    const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
-    const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
-
-    const fetchQuestionnaireOrders = async(): Promise<IQuestionnaireOrder[]> => {
-      const response = await axios.get(`${questionnaireServerOrigin}/questionnaire-order/index`);
-      return response.data.questionnaireOrders;
-    };
-
-    const saveUnfinishedOrders = async(questionnaireOrders: IQuestionnaireOrder[]) => {
-      const currentDate = new Date(Date.now());
-      const unfinishedOrders = questionnaireOrders.filter(order => new Date(order.showUntil) > currentDate);
-      await QuestionnaireOrder.insertMany(unfinishedOrders);
-    };
-
-    const changeSkippedAnswerStatusToNotAnswered = async() => {
-      await QuestionnaireAnswerStatus.updateMany(
-        { status: StatusType.skipped },
-        { status: StatusType.not_answered },
-      );
-    };
-
-    const resendQuestionnaireAnswers = async() => {
-      const questionnaireAnswers = await QuestionnaireAnswer.find()
-        .select('-_id -answers._id  -growiInfo._id -userInfo._id')
-        .lean();
-      const proactiveQuestionnaireAnswers = await ProactiveQuestionnaireAnswer.find()
-        .select('-_id -growiInfo._id -userInfo._id')
-        .lean();
-
-      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/batch`, {
-        // convert to legacy format
-        questionnaireAnswers: questionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
-      })
-        .then(async() => {
-          await QuestionnaireAnswer.deleteMany();
-        });
-      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive/batch`, {
-        // convert to legacy format
-        proactiveQuestionnaireAnswers: proactiveQuestionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
-      })
-        .then(async() => {
-          await ProactiveQuestionnaireAnswer.deleteMany();
-        });
-    };
-
-    const questionnaireOrders: IQuestionnaireOrder[] = await fetchQuestionnaireOrders();
-
-    resendQuestionnaireAnswers();
-
-    // reset QuestionnaireOrder collection and save unfinished ones that exist on questionnaire server
-    await QuestionnaireOrder.deleteMany();
-    await saveUnfinishedOrders(questionnaireOrders);
-
-    await changeSkippedAnswerStatusToNotAnswered();
-  }
-
-  private async sleepBeforeJob() {
-    const maxHoursUntilRequest = configManager.getConfig('app:questionnaireCronMaxHoursUntilRequest');
-    const maxSecondsUntilRequest = maxHoursUntilRequest * 60 * 60;
-
-    const secToSleep = getRandomIntInRange(0, maxSecondsUntilRequest);
-    await this.sleep(secToSleep * 1000);
-  }
-
-}
-
-const questionnaireCronService = new QuestionnaireCronService();
-
-export default questionnaireCronService;

+ 0 - 301
apps/app/src/features/questionnaire/server/service/questionnaire.integ.ts

@@ -1,301 +0,0 @@
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-import { mock } from 'vitest-mock-extended';
-
-import pkg from '^/package.json';
-
-
-import type UserEvent from '~/server/events/user';
-import { configManager } from '~/server/service/config-manager';
-
-import type Crowi from '../../../../server/crowi';
-import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import { UserType } from '../../interfaces/user-info';
-import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import QuestionnaireOrder from '../models/questionnaire-order';
-
-import QuestionnaireService from './questionnaire';
-
-
-describe('QuestionnaireService', () => {
-  const appVersion = pkg.version;
-
-  let questionnaireService: QuestionnaireService;
-
-  let User;
-  let user;
-
-  beforeAll(async() => {
-
-    await configManager.loadConfigs();
-
-    const crowiMock = mock<Crowi>({
-      version: appVersion,
-      event: vi.fn().mockImplementation((eventName) => {
-        if (eventName === 'user') {
-          return mock<UserEvent>({
-            on: vi.fn(),
-          });
-        }
-      }),
-    });
-    const userModelFactory = (await import('~/server/models/user')).default;
-    User = userModelFactory(crowiMock);
-
-    await User.deleteMany({}); // clear users
-    user = await User.create({
-      name: 'Example for Questionnaire Service Test',
-      username: 'questionnaire test user',
-      email: 'questionnaireTestUser@example.com',
-      password: 'usertestpass',
-      createdAt: '2000-01-01',
-    });
-
-    questionnaireService = new QuestionnaireService(crowiMock);
-  });
-
-  describe('getUserInfo', () => {
-    test('Should get correct user info when user given', () => {
-      const userInfo = questionnaireService.getUserInfo(user, 'growiurlhashfortest');
-      expect(userInfo).not.toBeNull();
-      assert(userInfo != null);
-
-      expect(userInfo.type).equal(UserType.general);
-      assert(userInfo.type === UserType.general);
-
-      expect(userInfo.userIdHash).toBeTruthy();
-      expect(userInfo.userIdHash).not.toBe(user._id);
-
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      delete (userInfo as any).userIdHash;
-
-      expect(userInfo).toEqual({ type: 'general', userCreatedAt: new Date('2000-01-01') });
-    });
-
-    test('Should get correct user info when user is null', () => {
-      const userInfo = questionnaireService.getUserInfo(null, '');
-      expect(userInfo).toEqual({ type: 'guest' });
-    });
-  });
-
-  describe('getQuestionnaireOrdersToShow', () => {
-    let doc1;
-    let doc2;
-    let doc3;
-    let doc4;
-    let doc5;
-    let doc6;
-    let doc7;
-    let doc8;
-    let doc9;
-    let doc10;
-    let doc11;
-    let doc12;
-
-    beforeAll(async() => {
-      const questionnaireToBeShown = {
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2022-12-11',
-        showUntil: '2100-12-12',
-        condition: {
-          user: {
-            types: ['general'],
-            daysSinceCreation: {
-              moreThanOrEqualTo: 365,
-              lessThanOrEqualTo: 365 * 1000,
-            },
-          },
-          growi: {
-            types: ['on-premise'],
-            versionRegExps: [appVersion],
-          },
-        },
-        createdAt: '2023-01-01',
-        updatedAt: '2023-01-01',
-      };
-
-      // insert initial db data
-      doc1 = await QuestionnaireOrder.create(questionnaireToBeShown);
-      // insert finished data
-      doc2 = await QuestionnaireOrder.create({
-        ...questionnaireToBeShown,
-        showFrom: '2020-12-11',
-        showUntil: '2021-12-12',
-      });
-      // insert data for admin or guest
-      doc3 = await QuestionnaireOrder.create({
-        ...questionnaireToBeShown,
-        condition: {
-          user: {
-            types: ['admin', 'guest'],
-          },
-          growi: {
-            types: ['on-premise'],
-            versionRegExps: [appVersion],
-          },
-        },
-      });
-      // insert answered data
-      doc4 = await QuestionnaireOrder.create(questionnaireToBeShown);
-      // insert skipped data
-      doc5 = await QuestionnaireOrder.create(questionnaireToBeShown);
-      // insert denied data
-      doc6 = await QuestionnaireOrder.create(questionnaireToBeShown);
-      // insert data for different growi type
-      doc7 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-            },
-            growi: {
-              types: ['cloud'],
-              versionRegExps: [appVersion],
-            },
-          },
-        },
-      );
-      // insert data for different growi version
-      doc8 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: ['1.0.0-alpha'],
-            },
-          },
-        },
-      );
-      // insert data for users that used GROWI for less than or equal to a year
-      doc9 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-              daysSinceCreation: {
-                lessThanOrEqualTo: 365,
-              },
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: [appVersion],
-            },
-          },
-        },
-      );
-      // insert data for users that used GROWI for more than or equal to 1000 years
-      doc10 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-              daysSinceCreation: {
-                moreThanOrEqualTo: 365 * 1000,
-              },
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: [appVersion],
-            },
-          },
-        },
-      );
-      // insert data for users that used GROWI for more than a month and less than 6 months
-      doc11 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-              daysSinceCreation: {
-                moreThanOrEqualTo: 30,
-                lessThanOrEqualTo: 30 * 6,
-              },
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: [appVersion],
-            },
-          },
-        },
-      );
-
-      await QuestionnaireAnswerStatus.insertMany([
-        {
-          user: user._id,
-          questionnaireOrderId: doc4._id,
-          status: StatusType.answered,
-        },
-        {
-          user: user._id,
-          questionnaireOrderId: doc5._id,
-          status: StatusType.skipped,
-        },
-        {
-          user: user._id,
-          questionnaireOrderId: doc6._id,
-          status: StatusType.skipped,
-        },
-      ]);
-    });
-
-    test('Should get questionnaire orders to show', async() => {
-      const growiInfo = mock<IGrowiInfo<IGrowiAppAdditionalInfo>>({
-        type: 'on-premise',
-        version: appVersion,
-      });
-      const userInfo = questionnaireService.getUserInfo(user, 'appSiteUrlHashed');
-
-      const questionnaireOrderDocuments = await questionnaireService.getQuestionnaireOrdersToShow(userInfo, growiInfo, user._id);
-
-      expect(questionnaireOrderDocuments[0].toObject()).toMatchObject(
-        {
-          __v: 0,
-          shortTitle: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          title: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          showFrom: new Date('2022-12-11'),
-          showUntil: new Date('2100-12-12'),
-          questions: [],
-          condition: {
-            user: {
-              types: ['general'],
-              daysSinceCreation: {
-                moreThanOrEqualTo: 365,
-                lessThanOrEqualTo: 365 * 1000,
-              },
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: [appVersion],
-            },
-          },
-          createdAt: new Date('2023-01-01'),
-          updatedAt: new Date('2023-01-01'),
-        },
-      );
-
-    });
-
-  });
-
-});

+ 0 - 74
apps/app/src/features/questionnaire/server/service/questionnaire.ts

@@ -1,74 +0,0 @@
-import crypto from 'crypto';
-
-import type { IUserHasId } from '@growi/core';
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type Crowi from '~/server/crowi';
-import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-import loggerFactory from '~/utils/logger';
-
-import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import { type IUserInfo, UserType } from '../../interfaces/user-info';
-import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import type { QuestionnaireOrderDocument } from '../models/questionnaire-order';
-import QuestionnaireOrder from '../models/questionnaire-order';
-import { isShowableCondition } from '../util/condition';
-
-
-const logger = loggerFactory('growi:service:questionnaire');
-
-class QuestionnaireService {
-
-  crowi: Crowi;
-
-  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  constructor(crowi: Crowi) {
-    this.crowi = crowi;
-  }
-
-  getUserInfo(user: IUserHasId | null, appSiteUrlHashed: string): IUserInfo {
-    if (user != null) {
-      const hasher = crypto.createHmac('sha256', appSiteUrlHashed);
-      hasher.update(user._id.toString());
-
-      return {
-        userIdHash: hasher.digest('hex'),
-        type: user.admin ? UserType.admin : UserType.general,
-        userCreatedAt: user.createdAt,
-      };
-    }
-
-    return { type: UserType.guest };
-  }
-
-  async getQuestionnaireOrdersToShow(
-      userInfo: IUserInfo, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>, userId: ObjectIdLike | null,
-  ): Promise<QuestionnaireOrderDocument[]> {
-    const currentDate = new Date();
-
-    let questionnaireOrders = await QuestionnaireOrder.find({
-      showUntil: {
-        $gte: currentDate,
-      },
-    });
-
-    if (userId != null) {
-      const statuses = await QuestionnaireAnswerStatus.find({ userId, questionnaireOrderId: { $in: questionnaireOrders.map(d => d._id) } });
-
-      questionnaireOrders = questionnaireOrders.filter((order) => {
-        const status = statuses.find(s => s.questionnaireOrderId.toString() === order._id.toString());
-
-        return !status || status?.status === StatusType.not_answered;
-      });
-    }
-
-    return questionnaireOrders
-      .filter((order) => {
-        return isShowableCondition(order, userInfo, growiInfo);
-      });
-  }
-
-}
-
-export default QuestionnaireService;

+ 0 - 70
apps/app/src/features/questionnaire/server/util/condition.ts

@@ -1,70 +0,0 @@
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type { ICondition } from '../../interfaces/condition';
-import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
-import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
-import type { IUserInfo } from '../../interfaces/user-info';
-import { UserType } from '../../interfaces/user-info';
-
-
-const checkUserInfo = (condition: ICondition, userInfo: IUserInfo): boolean => {
-  const { user: { types, daysSinceCreation } } = condition;
-
-  if (!types.includes(userInfo.type)) {
-    return false;
-  }
-
-  // Check if "time passed since user creation" is between specified range
-  if (userInfo.type !== UserType.guest) {
-    const createdAt = userInfo.userCreatedAt;
-    const moreThanOrEqualTo = daysSinceCreation?.moreThanOrEqualTo;
-    const lessThanOrEqualTo = daysSinceCreation?.lessThanOrEqualTo;
-    const currentDate = new Date();
-
-    const isValidLeftThreshold = (() => {
-      if (moreThanOrEqualTo == null) {
-        return true;
-      }
-      const leftThreshold = new Date(createdAt.getTime() + 60 * 1000 * 60 * 24 * moreThanOrEqualTo);
-      return leftThreshold <= currentDate;
-    })();
-    const isValidRightThreshold = (() => {
-      if (lessThanOrEqualTo == null) {
-        return true;
-      }
-      const rightThreshold = new Date(createdAt.getTime() + 60 * 1000 * 60 * 24 * lessThanOrEqualTo);
-      return currentDate <= rightThreshold;
-    })();
-
-    return isValidLeftThreshold && isValidRightThreshold;
-  }
-
-  return true;
-};
-
-const checkGrowiInfo = (condition: ICondition, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>): boolean => {
-  const { growi: { types, versionRegExps } } = condition;
-
-  if (!types.includes(growiInfo.type)) {
-    return false;
-  }
-
-  if (!versionRegExps.some(rs => new RegExp(rs).test(growiInfo.version))) {
-    return false;
-  }
-
-  return true;
-};
-
-export const isShowableCondition = (order: IQuestionnaireOrder, userInfo: IUserInfo, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>): boolean => {
-  const { condition } = order;
-
-  if (!checkUserInfo(condition, userInfo)) {
-    return false;
-  }
-  if (!checkGrowiInfo(condition, growiInfo)) {
-    return false;
-  }
-
-  return true;
-};

+ 0 - 128
apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.spec.ts

@@ -1,128 +0,0 @@
-import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-import { GrowiWikiType } from '@growi/core/dist/interfaces';
-import {
-  describe, test, expect,
-} from 'vitest';
-import { mock } from 'vitest-mock-extended';
-
-import { AttachmentMethodType } from '../../../../interfaces/attachment';
-import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
-
-import { convertToLegacyFormat } from './convert-to-legacy-format';
-
-describe('convertToLegacyFormat', () => {
-  test('should return same object when input is already in legacy format', () => {
-    const growiInfoLegacy: IGrowiAppInfoLegacy = {
-      version: '1.0.0',
-      appSiteUrl: 'https://example.com',
-      appSiteUrlHashed: '100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9',
-      serviceInstanceId: 'service-instance-id',
-      type: GrowiServiceType.cloud,
-      wikiType: GrowiWikiType.open,
-      deploymentType: GrowiDeploymentType.others,
-      osInfo: {
-        type: 'Linux',
-        platform: 'linux',
-        arch: 'x64',
-        totalmem: 8589934592,
-      },
-
-      // legacy properties
-      installedAt: new Date(),
-      installedAtByOldestUser: new Date(),
-      currentUsersCount: 1,
-      currentActiveUsersCount: 1,
-      attachmentType: AttachmentMethodType.local,
-    };
-
-    const legacyData = {
-      someData: 'test',
-      growiInfo: growiInfoLegacy,
-    };
-
-    const result = convertToLegacyFormat(legacyData);
-    expect(result).toStrictEqual(legacyData);
-  });
-
-  test('should convert new format to legacy format', () => {
-    const installedAt = new Date();
-    const installedAtByOldestUser = new Date();
-
-    const growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo> = {
-      version: '1.0.0',
-      appSiteUrl: 'https://example.com',
-      serviceInstanceId: 'service-instance-id',
-      type: GrowiServiceType.cloud,
-      wikiType: GrowiWikiType.open,
-      deploymentType: GrowiDeploymentType.others,
-      osInfo: {
-        type: 'Linux',
-        platform: 'linux',
-        arch: 'x64',
-        totalmem: 8589934592,
-      },
-      additionalInfo: {
-        installedAt,
-        installedAtByOldestUser,
-        currentUsersCount: 1,
-        currentActiveUsersCount: 1,
-        attachmentType: AttachmentMethodType.local,
-      },
-    };
-    const newFormatData = {
-      someData: 'test',
-      growiInfo,
-    };
-
-    const growiInfoLegacy: IGrowiAppInfoLegacy = {
-      version: '1.0.0',
-      appSiteUrl: 'https://example.com',
-      appSiteUrlHashed: '100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9',
-      serviceInstanceId: 'service-instance-id',
-      type: GrowiServiceType.cloud,
-      wikiType: GrowiWikiType.open,
-      deploymentType: GrowiDeploymentType.others,
-      osInfo: {
-        type: 'Linux',
-        platform: 'linux',
-        arch: 'x64',
-        totalmem: 8589934592,
-      },
-
-      // legacy properties
-      installedAt,
-      installedAtByOldestUser,
-      currentUsersCount: 1,
-      currentActiveUsersCount: 1,
-      attachmentType: AttachmentMethodType.local,
-    };
-    const expected = {
-      someData: 'test',
-      growiInfo: growiInfoLegacy,
-    };
-
-    const result = convertToLegacyFormat(newFormatData);
-    expect(result).toStrictEqual(expected);
-  });
-
-  test('should convert new format and omit appSiteUrl', () => {
-    // arrange
-    const growiInfo = mock<IGrowiInfo<IGrowiAppAdditionalInfo>>({
-      appSiteUrl: 'https://example.com',
-      additionalInfo: {
-        installedAt: new Date(),
-        installedAtByOldestUser: new Date(),
-        currentUsersCount: 1,
-        currentActiveUsersCount: 1,
-        attachmentType: AttachmentMethodType.local,
-      },
-    });
-
-    // act
-    const result = convertToLegacyFormat({ growiInfo }, true);
-
-    // assert
-    expect(result.growiInfo.appSiteUrl).toBeUndefined();
-  });
-});

+ 0 - 40
apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.ts

@@ -1,40 +0,0 @@
-import assert from 'assert';
-import crypto from 'crypto';
-
-import type { IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
-
-
-type IHasGrowiAppInfoLegacy<T> = T & {
-  growiInfo: IGrowiAppInfoLegacy;
-};
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-function isLegacy<T extends { growiInfo: any }>(data: T): data is IHasGrowiAppInfoLegacy<T> {
-  return !('additionalInfo' in data.growiInfo);
-}
-
-export function getSiteUrlHashed(siteUrl: string): string {
-  const hasher = crypto.createHash('sha256');
-  hasher.update(siteUrl);
-  return hasher.digest('hex');
-}
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function convertToLegacyFormat<T extends { growiInfo: any }>(questionnaireAnswer: T, isAppSiteUrlHashed = false): IHasGrowiAppInfoLegacy<T> {
-  if (isLegacy(questionnaireAnswer)) {
-    return questionnaireAnswer;
-  }
-
-  const { additionalInfo, appSiteUrl, ...rest } = questionnaireAnswer.growiInfo;
-  assert(additionalInfo != null);
-
-  return {
-    ...questionnaireAnswer,
-    growiInfo: {
-      appSiteUrl: isAppSiteUrlHashed ? undefined : appSiteUrl,
-      appSiteUrlHashed: getSiteUrlHashed(appSiteUrl),
-      ...rest,
-      ...additionalInfo,
-    },
-  };
-}