diff --git a/db/init/init_accounts_auto_increment.sql b/db/init/init_accounts_auto_increment.sql
new file mode 100644
index 0000000..7d0aa25
--- /dev/null
+++ b/db/init/init_accounts_auto_increment.sql
@@ -0,0 +1,4 @@
+-- [OMDS_IS-231] アカウントIDの開始番号調整 | 課題の表示 | Backlog 対応
+-- IDからアカウント数が推測されるため、ユーザ指定の任意値を最初の番号とする
+-- 一度しか実行しないため、migrate fileではなくDBの初期値として扱う。移行時の実行を想定
+ALTER TABLE accounts AUTO_INCREMENT = 853211;
diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts
index da0d9b9..50d91db 100644
--- a/dictation_client/src/common/errors/code.ts
+++ b/dictation_client/src/common/errors/code.ts
@@ -54,6 +54,8 @@ export const errorCodes = [
"E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
"E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
"E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
+ "E010908", // タイピストグループ不在エラー
+ "E010909", // タイピストグループ名重複エラー
"E011001", // ワークタイプ重複エラー
"E011002", // ワークタイプ登録上限超過エラー
"E011003", // ワークタイプ不在エラー
diff --git a/dictation_client/src/components/auth/constants.ts b/dictation_client/src/components/auth/constants.ts
index ef98041..dd8ec52 100644
--- a/dictation_client/src/components/auth/constants.ts
+++ b/dictation_client/src/components/auth/constants.ts
@@ -43,7 +43,12 @@ export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [
* ローカルストレージに残すキー類
* @const {string[]}
*/
-export const KEYS_TO_PRESERVE = ["accessToken", "refreshToken", "displayInfo"];
+export const KEYS_TO_PRESERVE = [
+ "accessToken",
+ "refreshToken",
+ "displayInfo",
+ "sortCriteria",
+];
/**
* アクセストークンを更新する基準の秒数
diff --git a/dictation_client/src/features/dictation/constants.ts b/dictation_client/src/features/dictation/constants.ts
index 69eae60..f7e22d1 100644
--- a/dictation_client/src/features/dictation/constants.ts
+++ b/dictation_client/src/features/dictation/constants.ts
@@ -28,6 +28,13 @@ export const SORTABLE_COLUMN = {
export type SortableColumnType =
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN];
+export const isSortableColumnType = (
+ value: string
+): value is SortableColumnType => {
+ const arg = value as SortableColumnType;
+ return Object.values(SORTABLE_COLUMN).includes(arg);
+};
+
export type SortableColumnList =
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN];
@@ -38,6 +45,10 @@ export const DIRECTION = {
export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION];
+// DirectionTypeの型チェック関数
+export const isDirectionType = (arg: string): arg is DirectionType =>
+ arg in DIRECTION;
+
export interface DisplayInfoType {
JobNumber: boolean;
Status: boolean;
diff --git a/dictation_client/src/features/dictation/operations.ts b/dictation_client/src/features/dictation/operations.ts
index 23b98f4..99b426e 100644
--- a/dictation_client/src/features/dictation/operations.ts
+++ b/dictation_client/src/features/dictation/operations.ts
@@ -280,7 +280,6 @@ export const playbackAsync = createAsyncThunk<
direction: DirectionType;
paramName: SortableColumnType;
audioFileId: number;
- isTypist: boolean;
},
{
// rejectした時の返却値の型
@@ -289,7 +288,7 @@ export const playbackAsync = createAsyncThunk<
};
}
>("dictations/playbackAsync", async (args, thunkApi) => {
- const { audioFileId, direction, paramName, isTypist } = args;
+ const { audioFileId, direction, paramName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
@@ -300,15 +299,12 @@ export const playbackAsync = createAsyncThunk<
const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config);
try {
- // ユーザーがタイピストである場合に、ソート条件を保存する
- if (isTypist) {
- await usersApi.updateSortCriteria(
- { direction, paramName },
- {
- headers: { authorization: `Bearer ${accessToken}` },
- }
- );
- }
+ await usersApi.updateSortCriteria(
+ { direction, paramName },
+ {
+ headers: { authorization: `Bearer ${accessToken}` },
+ }
+ );
await tasksApi.checkout(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
diff --git a/dictation_client/src/features/license/licenseOrder/operations.ts b/dictation_client/src/features/license/licenseOrder/operations.ts
index aaf3a84..abd5981 100644
--- a/dictation_client/src/features/license/licenseOrder/operations.ts
+++ b/dictation_client/src/features/license/licenseOrder/operations.ts
@@ -62,6 +62,12 @@ export const orderLicenseAsync = createAsyncThunk<
);
}
+ if (error.code === "E010501") {
+ errorMessage = getTranslationID(
+ "licenseOrderPage.message.dealerNotFoundError"
+ );
+ }
+
thunkApi.dispatch(
openSnackbar({
level: "error",
diff --git a/dictation_client/src/features/workflow/typistGroup/operations.ts b/dictation_client/src/features/workflow/typistGroup/operations.ts
index a98ff1b..70594c6 100644
--- a/dictation_client/src/features/workflow/typistGroup/operations.ts
+++ b/dictation_client/src/features/workflow/typistGroup/operations.ts
@@ -122,11 +122,17 @@ export const createTypistGroupAsync = createAsyncThunk<
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
-
- const message =
- error.statusCode === 400
- ? getTranslationID("typistGroupSetting.message.groupSaveFailedError")
- : getTranslationID("common.message.internalServerError");
+ let message = getTranslationID("common.message.internalServerError");
+ if (error.code === "E010204") {
+ message = getTranslationID(
+ "typistGroupSetting.message.groupSaveFailedError"
+ );
+ }
+ if (error.code === "E010909") {
+ message = getTranslationID(
+ "typistGroupSetting.message.GroupNameAlreadyExistError"
+ );
+ }
thunkApi.dispatch(
openSnackbar({
@@ -242,10 +248,17 @@ export const updateTypistGroupAsync = createAsyncThunk<
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
- const message =
- error.statusCode === 400
- ? getTranslationID("typistGroupSetting.message.groupSaveFailedError")
- : getTranslationID("common.message.internalServerError");
+ let message = getTranslationID("common.message.internalServerError");
+ if (error.code === "E010204" || error.code === "E010908") {
+ message = getTranslationID(
+ "typistGroupSetting.message.groupSaveFailedError"
+ );
+ }
+ if (error.code === "E010909") {
+ message = getTranslationID(
+ "typistGroupSetting.message.GroupNameAlreadyExistError"
+ );
+ }
thunkApi.dispatch(
openSnackbar({
diff --git a/dictation_client/src/pages/AccountPage/index.tsx b/dictation_client/src/pages/AccountPage/index.tsx
index 6fd0877..8e3a149 100644
--- a/dictation_client/src/pages/AccountPage/index.tsx
+++ b/dictation_client/src/pages/AccountPage/index.tsx
@@ -191,23 +191,32 @@ const AccountPage: React.FC = (): JSX.Element => {
)}
{isTier5 && (
-
+ {t(
+ getTranslationID(
+ "accountPage.text.dealerManagementAnnotation"
+ )
+ )}
+
+ )}
{isTier5 && (
diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx
index bb70c47..1d3abcc 100644
--- a/dictation_client/src/pages/DictationPage/index.tsx
+++ b/dictation_client/src/pages/DictationPage/index.tsx
@@ -33,6 +33,8 @@ import {
playbackAsync,
cancelAsync,
PRIORITY,
+ isSortableColumnType,
+ isDirectionType,
} from "features/dictation";
import { getTranslationID } from "translation";
import { Task } from "api/api";
@@ -242,6 +244,12 @@ const DictationPage: React.FC = (): JSX.Element => {
dispatch(changeDirection({ direction: currentDirection }));
dispatch(changeParamName({ paramName }));
+ // ローカルストレージにソート情報を保存する
+ localStorage.setItem(
+ "sortCriteria",
+ `direction:${currentDirection},paramName:${paramName}`
+ );
+
const filter = getFilter(
filterUploaded,
filterInProgress,
@@ -348,10 +356,11 @@ const DictationPage: React.FC = (): JSX.Element => {
audioFileId,
direction: sortDirection,
paramName: sortableParamName,
- isTypist,
})
);
if (meta.requestStatus === "fulfilled") {
+ // ローカルストレージにソート情報を削除する
+ localStorage.removeItem("sortCriteria");
const filter = getFilter(
filterUploaded,
filterInProgress,
@@ -388,7 +397,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filterInProgress,
filterPending,
filterUploaded,
- isTypist,
sortDirection,
sortableParamName,
t,
@@ -522,13 +530,39 @@ const DictationPage: React.FC = (): JSX.Element => {
dispatch(changeDisplayInfo({ column: displayInfo }));
const filter = getFilter(true, true, true, true, false);
+
const { meta, payload } = await dispatch(getSortColumnAsync());
if (
meta.requestStatus === "fulfilled" &&
payload &&
!("error" in payload)
) {
- const { direction, paramName } = payload;
+ // ソート情報をローカルストレージから取得する
+ const sortColumnValue = localStorage.getItem("sortCriteria") ?? "";
+ let direction: DirectionType;
+ let paramName: SortableColumnType;
+ if (sortColumnValue === "") {
+ direction = payload.direction;
+ paramName = payload.paramName;
+ } else {
+ // ソート情報をDirectionとParamNameに分割する
+ const sortColumn = sortColumnValue?.split(",");
+ const localStorageDirection = sortColumn[0].split(":")[1] ?? "";
+
+ const localStorageParamName = sortColumn[1]?.split(":")[1] ?? "";
+
+ // 正常なソート情報がローカルストレージに存在する場合はローカルストレージの情報を使用する
+ direction = isDirectionType(localStorageDirection)
+ ? localStorageDirection
+ : payload.direction;
+ paramName = isSortableColumnType(localStorageParamName)
+ ? localStorageParamName
+ : payload.paramName;
+
+ dispatch(changeDirection({ direction }));
+ dispatch(changeParamName({ paramName }));
+ }
+
dispatch(
listTasksAsync({
limit: LIMIT_TASK_NUM,
@@ -1082,7 +1116,13 @@ const DictationPage: React.FC = (): JSX.Element => {
{(isChangeTranscriptionistPopupOpen || !isLoading) &&
tasks.length !== 0 &&
tasks.map((x) => (
-
-
diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx
index 7f4934f..bf0f407 100644
--- a/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx
+++ b/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx
@@ -85,7 +85,7 @@ export const AddWorktypeIdPopup: React.FC = (
{
diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx
index 248005b..a3d4fee 100644
--- a/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx
+++ b/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx
@@ -84,7 +84,7 @@ export const EditWorktypeIdPopup: React.FC = (
{
diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss
index 2435346..c075fdc 100644
--- a/dictation_client/src/styles/app.module.scss
+++ b/dictation_client/src/styles/app.module.scss
@@ -1343,23 +1343,23 @@ _:-ms-lang(x)::-ms-backdrop,
.tableHeader th .hasSort:hover::before {
opacity: 1;
}
-.tableHeader th .hasSort.isActiveAz::before {
- opacity: 1;
-}
-.tableHeader th .hasSort.isActiveAz:hover::before {
- border-top: none;
- border-right: 0.35rem transparent solid;
- border-bottom: 0.4rem #ffffff solid;
- border-left: 0.35rem transparent solid;
-}
.tableHeader th .hasSort.isActiveZa::before {
- border-top: none;
- border-right: 0.35rem transparent solid;
- border-bottom: 0.4rem #ffffff solid;
- border-left: 0.35rem transparent solid;
opacity: 1;
}
.tableHeader th .hasSort.isActiveZa:hover::before {
+ border-top: none;
+ border-right: 0.35rem transparent solid;
+ border-bottom: 0.4rem #ffffff solid;
+ border-left: 0.35rem transparent solid;
+}
+.tableHeader th .hasSort.isActiveAz::before {
+ border-top: none;
+ border-right: 0.35rem transparent solid;
+ border-bottom: 0.4rem #ffffff solid;
+ border-left: 0.35rem transparent solid;
+ opacity: 1;
+}
+.tableHeader th .hasSort.isActiveAz:hover::before {
border-top: 0.4rem #ffffff solid;
border-right: 0.35rem transparent solid;
border-bottom: none;
@@ -1632,8 +1632,31 @@ _:-ms-lang(x)::-ms-backdrop,
.account .listVertical dd .formInput {
max-width: 100%;
}
+.account .listVertical dd.full {
+ width: 100%;
+ padding-top: 0;
+ background: none;
+}
+.account .listVertical dd.full.odd {
+ background: #f5f5f5;
+}
+.account .listVertical dd.formComment {
+ text-align: left;
+ font-size: 0.9rem;
+ word-break: break-word;
+}
+.account .box100 .formComment {
+ display: block;
+ width: 600px;
+ text-align: left;
+}
.account .box100.alignRight {
width: calc(1200px + 3rem);
+ text-align: right;
+}
+.account .box100.alignRight .formComment {
+ margin-left: 648px;
+ text-align: right;
}
.menuAction {
@@ -2306,7 +2329,8 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover {
- background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
+ background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left
+ center;
background-size: 1.3rem;
}
.formChange ul.chooseMember li input:checked + label,
@@ -2317,8 +2341,8 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input:checked + label:hover,
.formChange ul.holdMember li input:checked + label:hover {
- background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right
- center;
+ background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
+ right center;
background-size: 1.3rem;
}
.formChange > p {
@@ -2471,7 +2495,8 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover {
- background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
+ background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left
+ center;
background-size: 1.3rem;
}
.formChange ul.chooseMember li input:checked + label,
@@ -2482,8 +2507,8 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input:checked + label:hover,
.formChange ul.holdMember li input:checked + label:hover {
- background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right
- center;
+ background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
+ right center;
background-size: 1.3rem;
}
.formChange > p {
diff --git a/dictation_client/src/styles/app.module.scss.d.ts b/dictation_client/src/styles/app.module.scss.d.ts
index c4b1492..68fcbca 100644
--- a/dictation_client/src/styles/app.module.scss.d.ts
+++ b/dictation_client/src/styles/app.module.scss.d.ts
@@ -89,8 +89,8 @@ declare const classNames: {
readonly snackbarIcon: "snackbarIcon";
readonly snackbarIconClose: "snackbarIconClose";
readonly hasSort: "hasSort";
- readonly isActiveAz: "isActiveAz";
readonly isActiveZa: "isActiveZa";
+ readonly isActiveAz: "isActiveAz";
readonly noLine: "noLine";
readonly home: "home";
readonly pgHome: "pgHome";
@@ -107,6 +107,7 @@ declare const classNames: {
readonly clm0: "clm0";
readonly menuInTable: "menuInTable";
readonly isSelected: "isSelected";
+ readonly odd: "odd";
readonly alignRight: "alignRight";
readonly menuAction: "menuAction";
readonly inTable: "inTable";
diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json
index 3f82e06..bff0bba 100644
--- a/dictation_client/src/translation/de.json
+++ b/dictation_client/src/translation/de.json
@@ -24,7 +24,7 @@
"headerDictations": "Diktate",
"headerWorkflow": "Arbeitsablauf",
"headerPartners": "Partner",
- "headerSupport": "(de)Support",
+ "headerSupport": "Support",
"tier1": "Admin",
"tier2": "BC",
"tier3": "Verteiler",
@@ -76,6 +76,7 @@
"linkOfEula": "Klicken Sie hier, um die Endbenutzer-Lizenzvereinbarung zu lesen.",
"linkOfPrivacyNotice": "Klicken Sie hier, um die Datenschutzerklärung zu lesen.",
"forOdms": "für ODMS Cloud.",
+ "termsCheckBox": "Ja, ich stimme den Nutzungsbedingungen zu.",
"createAccountButton": "Einreichen"
}
},
@@ -127,7 +128,14 @@
"authorIdIncorrectError": "Das Format der Autoren-ID ist ungültig. Als Autoren-ID können nur alphanumerische Zeichen und „_“ eingegeben werden.",
"roleChangeError": "Die Benutzerrolle kann nicht geändert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.",
"encryptionPasswordCorrectError": "Das Verschlüsselungskennwort entspricht nicht den Regeln.",
- "alreadyLicenseDeallocatedError": "Die zugewiesene Lizenz wurde bereits storniert. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen."
+ "alreadyLicenseDeallocatedError": "Die zugewiesene Lizenz wurde bereits storniert. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.",
+ "UserDeletionLicenseActiveError": "(de)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。",
+ "TypistDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。",
+ "AdminUserDeletionError": "(de)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。",
+ "TypistUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。",
+ "AuthorUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。",
+ "TypistUserDeletionTranscriptionistGroupError": "(de)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。",
+ "AuthorDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。"
},
"label": {
"title": "Benutzer",
@@ -179,8 +187,8 @@
"storageSize": "Lagerung verfügbar",
"usedSize": "Gebrauchter Lagerung",
"storageAvailable": "Speicher nicht verfügbar (Menge überschritten)",
- "licenseLabel": "(de)License",
- "storageLabel": "(de)Storage"
+ "licenseLabel": "Lizenz",
+ "storageLabel": "Lagerung"
}
},
"licenseOrderPage": {
@@ -189,7 +197,8 @@
"poNumberIncorrectError": "Das Format der Bestellnummer ist ungültig. Für die Bestellnummer können nur alphanumerische Zeichen eingegeben werden.",
"newOrderIncorrectError": "Bitte geben Sie für die neue Bestellung eine Zahl größer oder gleich 1 ein.",
"confirmOrder": "Möchten Sie eine Bestellung aufgeben?",
- "poNumberConflictError": "Die eingegebene Bestellnummer existiert bereits. Bitte geben Sie eine andere Bestellnummer ein."
+ "poNumberConflictError": "Die eingegebene Bestellnummer existiert bereits. Bitte geben Sie eine andere Bestellnummer ein.",
+ "dealerNotFoundError": "(de)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。"
},
"label": {
"title": "Lizenz bestellen",
@@ -205,8 +214,9 @@
"noPlaybackAuthorization": "Sie haben keine Berechtigung zum Abspielen dieser Datei.",
"taskToPlaybackNoExists": "Die Datei kann nicht abgespielt werden, da sie bereits transkribiert wurde oder nicht existiert.",
"taskNotEditable": "Der Transkriptionist kann nicht geändert werden, da die Transkription bereits ausgeführt wird oder die Datei nicht vorhanden ist. Bitte aktualisieren Sie den Bildschirm und prüfen Sie den aktuellen Status.",
- "backupFailedError": "(de)ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。",
- "cancelFailedError": "(de)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。"
+ "backupFailedError": "Der Prozess „Dateisicherung“ ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal. Wenn der Fehler weiterhin besteht, wenden Sie sich an Ihren Systemadministrator.",
+ "cancelFailedError": "Die Diktate konnten nicht gelöscht werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.",
+ "deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。"
},
"label": {
"title": "Diktate",
@@ -249,9 +259,9 @@
"deleteDictation": "Diktat löschen",
"selectedTranscriptionist": "Ausgewählter transkriptionist",
"poolTranscriptionist": "Transkriptionsliste",
- "fileBackup": "(de)File Backup",
- "downloadForBackup": "(de)Download for backup",
- "applications": "(de)Applications",
+ "fileBackup": "Dateisicherung",
+ "downloadForBackup": "Zur Sicherung herunterladen",
+ "applications": "Desktopanwendung",
"cancelDictation": "Transkription abbrechen"
}
},
@@ -414,7 +424,10 @@
},
"message": {
"selectedTypistEmptyError": "Um eine Transkriptionsgruppe zu speichern, müssen ein oder mehrere Transkriptionisten ausgewählt werden.",
- "groupSaveFailedError": "Die Schreibkraftgruppe konnte nicht gespeichert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen."
+ "groupSaveFailedError": "Die Transkriptionistengruppe konnte nicht gespeichert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.",
+ "GroupNameAlreadyExistError": "(de)このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。",
+ "deleteFailedWorkflowAssigned": "(de)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。",
+ "deleteFailedCheckoutPermissionExisted": "(de)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。"
}
},
"worktypeIdSetting": {
@@ -470,7 +483,7 @@
"addAccount": "Konto hinzufügen",
"name": "Name der Firma",
"category": "Kontoebene",
- "accountId": "Konto-ID",
+ "accountId": "Autoren-ID",
"country": "Land",
"primaryAdmin": "Hauptadministrator",
"email": "Email",
@@ -506,6 +519,9 @@
},
"message": {
"updateAccountFailedError": "Kontoinformationen konnten nicht gespeichert werden. Bitte aktualisieren Sie den Bildschirm und versuchen Sie es erneut."
+ },
+ "text": {
+ "dealerManagementAnnotation": "Durch die Aktivierung der Option „Erlauben Sie dem Händler, Änderungen vorzunehmen“ erklären Sie sich damit einverstanden, dass Ihr Händler die Rechte erhält, auf Ihr ODMS Cloud-Konto zuzugreifen, um in Ihrem Namen Lizenzen zu bestellen und Benutzer zu registrieren. Ihr Händler hat keinen Zugriff auf Sprachdateien oder Dokumente, die in Ihrem ODMS Cloud-Konto gespeichert sind."
}
},
"deleteAccountPopup": {
@@ -537,22 +553,22 @@
},
"supportPage": {
"label": {
- "title": "(de)Support",
- "howToUse": "(de)How to use the system",
+ "title": "Support",
+ "howToUse": "So verwenden Sie das System",
"supportPageEnglish": "OMDS Cloud User Guide",
- "supportPageGerman": "OMDS Cloud Benutzerhandbuch",
- "supportPageFrench": "OMDS Cloud Mode d'emploi",
- "supportPageSpanish": "OMDS Cloud Guía del usuario"
+ "supportPageGerman": "OMDS Cloud-Benutzerhandbuch",
+ "supportPageFrench": "Guía del usuario de la nube OMDS",
+ "supportPageSpanish": "Guide de l'utilisateur du cloud OMDS"
},
"text": {
- "notResolved": "(de)If the problem persists even after referring to the user guide, please contact a higher-level person in charge."
+ "notResolved": "Informationen zu den Funktionen der ODMS Cloud finden Sie im Benutzerhandbuch. Wenn Sie zusätzlichen Support benötigen, wenden Sie sich bitte an Ihren Administrator oder zertifizierten ODMS Cloud-Händler."
}
},
"filePropertyPopup": {
"label": {
- "general": "(de)General",
- "job": "(de)Job",
- "close": "(de)Close"
+ "general": "Allgemein",
+ "job": "Aufgabe",
+ "close": "Schließen"
}
}
}
diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json
index 6b68ea1..e0f1800 100644
--- a/dictation_client/src/translation/en.json
+++ b/dictation_client/src/translation/en.json
@@ -75,7 +75,7 @@
"password": "Password",
"linkOfEula": "Click here to read the End User License Agreement.",
"linkOfPrivacyNotice": "Click here to read the Privacy Notice.",
- "forOdms": "for ODMS Cloud.",
+ "forOdms": "for OMDS Cloud.",
"termsCheckBox": "Yes, I agree to the terms of use.",
"createAccountButton": "Submit"
}
@@ -128,7 +128,14 @@
"authorIdIncorrectError": "Author ID format is invalid. Only alphanumeric characters and \"_\" can be entered for Author ID.",
"roleChangeError": "Unable to change the User Role. The displayed information may be outdated, so please refresh the screen to see the latest status.",
"encryptionPasswordCorrectError": "Encryption password does not meet the rules.",
- "alreadyLicenseDeallocatedError": "Assigned license has already been canceled. The displayed information may be outdated, so please refresh the screen to see the latest status."
+ "alreadyLicenseDeallocatedError": "Assigned license has already been canceled. The displayed information may be outdated, so please refresh the screen to see the latest status.",
+ "UserDeletionLicenseActiveError": "ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。",
+ "TypistDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。",
+ "AdminUserDeletionError": "ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。",
+ "TypistUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。",
+ "AuthorUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。",
+ "TypistUserDeletionTranscriptionistGroupError": "ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。",
+ "AuthorDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。"
},
"label": {
"title": "User",
@@ -190,7 +197,8 @@
"poNumberIncorrectError": "PO Number format is not valid. Only alphanumeric characters can be entered for the PO Number.",
"newOrderIncorrectError": "Please enter a number greater than or equal to 1 for the New Order.",
"confirmOrder": "Would you like to place an order?",
- "poNumberConflictError": "PO Number entered already exists. Please enter a different PO Number."
+ "poNumberConflictError": "PO Number entered already exists. Please enter a different PO Number.",
+ "dealerNotFoundError": "ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。"
},
"label": {
"title": "Order License",
@@ -206,8 +214,9 @@
"noPlaybackAuthorization": "You do not have permission to playback this file.",
"taskToPlaybackNoExists": "The file cannot be played because it has already been transcribed or does not exist.",
"taskNotEditable": "The transcriptionist cannot be changed because the transcription is already in progress or the file does not exist. Please refresh the screen and check the latest status.",
- "backupFailedError": "ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。",
- "cancelFailedError": "タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。"
+ "backupFailedError": "The \"File Backup\" process has failed. Please try again later. If the error continues, contact your system administrator.",
+ "cancelFailedError": "Failed to delete the dictations. Please refresh your screen and try again.",
+ "deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。"
},
"label": {
"title": "Dictations",
@@ -252,7 +261,7 @@
"poolTranscriptionist": "Transcription List",
"fileBackup": "File Backup",
"downloadForBackup": "Download for backup",
- "applications": "Applications",
+ "applications": "Desktop Application",
"cancelDictation": "Cancel Transcription"
}
},
@@ -415,7 +424,10 @@
},
"message": {
"selectedTypistEmptyError": "One or more transcriptonist must be selected to save a transcrption group.",
- "groupSaveFailedError": "Typist Group could not be saved. The displayed information may be outdated, so please refresh the screen to see the latest status."
+ "groupSaveFailedError": "Transcriptionist Group could not be saved. The displayed information may be outdated, so please refresh the screen to see the latest status.",
+ "GroupNameAlreadyExistError": "このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。",
+ "deleteFailedWorkflowAssigned": "TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。",
+ "deleteFailedCheckoutPermissionExisted": "TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。"
}
},
"worktypeIdSetting": {
@@ -507,6 +519,9 @@
},
"message": {
"updateAccountFailedError": "Failed to save account information. Please refresh the screen and try again."
+ },
+ "text": {
+ "dealerManagementAnnotation": "By enabling the \"Dealer Management\" option, you are agreeing to allow your dealer to have the rights to access your ODMS Cloud account to order licenses and register users on your behalf. Your dealer will not have access to any voice file(s) or document(s) stored in your ODMS Cloud account."
}
},
"deleteAccountPopup": {
@@ -541,12 +556,12 @@
"title": "Support",
"howToUse": "How to use the system",
"supportPageEnglish": "OMDS Cloud User Guide",
- "supportPageGerman": "OMDS Cloud Benutzerhandbuch",
- "supportPageFrench": "OMDS Cloud Mode d'emploi",
- "supportPageSpanish": "OMDS Cloud Guía del usuario"
+ "supportPageGerman": "OMDS Cloud-Benutzerhandbuch",
+ "supportPageFrench": "Guía del usuario de la nube OMDS",
+ "supportPageSpanish": "Guide de l'utilisateur du cloud OMDS"
},
"text": {
- "notResolved": "If the problem persists even after referring to the user guide, please contact a higher-level person in charge."
+ "notResolved": "Please refer to the User Guide for information about the features of the ODMS Cloud. If you require additional support, please contact your administrator or certified ODMS Cloud reseller."
}
},
"filePropertyPopup": {
diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json
index 85cda56..564958a 100644
--- a/dictation_client/src/translation/es.json
+++ b/dictation_client/src/translation/es.json
@@ -24,11 +24,11 @@
"headerDictations": "Dictado",
"headerWorkflow": "flujo de trabajo",
"headerPartners": "Socios",
- "headerSupport": "(es)Support",
+ "headerSupport": "Soporte",
"tier1": "Admin",
"tier2": "BC",
"tier3": "Distribuidor",
- "tier4": "Concesionario",
+ "tier4": "Distribuidor",
"tier5": "Cliente",
"notSelected": "Ninguno",
"signOutButton": "cerrar sesión"
@@ -62,14 +62,14 @@
"title": "Crea tu cuenta",
"accountInfoTitle": "Información de Registro",
"countryExplanation": "Seleccione el país donde se encuentra. Si su país no aparece en la lista, seleccione el país más cercano.",
- "dealerExplanation": "Seleccione el concesionario al que le gustaría comprar la licencia.",
+ "dealerExplanation": "Seleccione el distribuidor al que le gustaría comprar la licencia.",
"adminInfoTitle": "Registre la información del administrador principal",
"passwordTerms": "Establezca una contraseña. La contraseña debe tener entre 8 y 25 caracteres y debe contener letras, números y símbolos. (Debe enumerar el símbolo compatible e indicar si se necesita una letra mayúscula)."
},
"label": {
"company": "Nombre de empresa",
"country": "País",
- "dealer": "Concesionario (Opcional)",
+ "dealer": "Distribuidor (Opcional)",
"adminName": "Nombre del administrador",
"email": "Dirección de correo electrónico",
"password": "Contraseña",
@@ -93,7 +93,7 @@
"label": {
"company": "Nombre de empresa",
"country": "País",
- "dealer": "Concesionario (Opcional)",
+ "dealer": "Distribuidor (Opcional)",
"adminName": "Nombre del administrador",
"email": "Dirección de correo electrónico",
"password": "Contraseña",
@@ -128,7 +128,14 @@
"authorIdIncorrectError": "El formato de ID del autor no es válido. Sólo se pueden ingresar caracteres alfanuméricos y \"_\" para la ID del autor.",
"roleChangeError": "No se puede cambiar la función de usuario. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente.",
"encryptionPasswordCorrectError": "La contraseña de cifrado no cumple con las reglas.",
- "alreadyLicenseDeallocatedError": "La licencia asignada ya ha sido cancelada. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente."
+ "alreadyLicenseDeallocatedError": "La licencia asignada ya ha sido cancelada. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente.",
+ "UserDeletionLicenseActiveError": "(es)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。",
+ "TypistDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。",
+ "AdminUserDeletionError": "(es)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。",
+ "TypistUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。",
+ "AuthorUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。",
+ "TypistUserDeletionTranscriptionistGroupError": "(es)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。",
+ "AuthorDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。"
},
"label": {
"title": "Usuario",
@@ -180,8 +187,8 @@
"storageSize": "Almacenamiento disponible",
"usedSize": "Almacenamiento utilizado",
"storageAvailable": "Almacenamiento no disponible (cantidad excedida)",
- "licenseLabel": "(es)License",
- "storageLabel": "(es)Storage"
+ "licenseLabel": "Licencia",
+ "storageLabel": "Almacenamiento"
}
},
"licenseOrderPage": {
@@ -190,7 +197,8 @@
"poNumberIncorrectError": "El formato del número de orden de compra no es válido. Sólo se pueden ingresar caracteres alfanuméricos para el número de orden de compra.",
"newOrderIncorrectError": "Ingrese un número mayor o igual a 1 para el Nuevo Pedido.",
"confirmOrder": "¿Quieres hacer un pedido?",
- "poNumberConflictError": "El número de orden de compra ingresado ya existe. Ingrese un número de orden de compra diferente."
+ "poNumberConflictError": "El número de orden de compra ingresado ya existe. Ingrese un número de orden de compra diferente.",
+ "dealerNotFoundError": "(es)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。"
},
"label": {
"title": "Licencia de pedido",
@@ -206,8 +214,9 @@
"noPlaybackAuthorization": "No tienes permiso para reproducir este archivo.",
"taskToPlaybackNoExists": "El archivo no se puede reproducir porque ya ha sido transcrito o no existe.",
"taskNotEditable": "No se puede cambiar el transcriptor porque la transcripción ya está en curso o el archivo no existe. Actualice la pantalla y verifique el estado más reciente.",
- "backupFailedError": "(es)ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。",
- "cancelFailedError": "(es)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。"
+ "backupFailedError": "El proceso de \"Copia de seguridad de archivos\" ha fallado. Por favor, inténtelo de nuevo más tarde. Si el error continúa, comuníquese con el administrador del sistema.",
+ "cancelFailedError": "No se pudieron eliminar los dictados. Actualice su pantalla e inténtelo nuevamente.",
+ "deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。"
},
"label": {
"title": "Dictado",
@@ -250,9 +259,9 @@
"deleteDictation": "Borrar dictado",
"selectedTranscriptionist": "Transcriptor seleccionado",
"poolTranscriptionist": "Lista de transcriptor",
- "fileBackup": "(es)File Backup",
- "downloadForBackup": "(es)Download for backup",
- "applications": "(es)Applications",
+ "fileBackup": "Copia de seguridad de archivos",
+ "downloadForBackup": "Descargar para respaldo",
+ "applications": "Aplicación de escritorio",
"cancelDictation": "Cancelar transcripción"
}
},
@@ -415,7 +424,10 @@
},
"message": {
"selectedTypistEmptyError": "Se deben seleccionar uno o más transcriptores para guardar un grupo de transcripción.",
- "groupSaveFailedError": "No se pudo guardar el grupo mecanógrafo. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente."
+ "groupSaveFailedError": "El grupo transcriptor no se pudo salvar. La información mostrada puede estar desactualizada. Así que actualice la pantalla para ver el estado más reciente.",
+ "GroupNameAlreadyExistError": "(es)このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。",
+ "deleteFailedWorkflowAssigned": "(es)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。",
+ "deleteFailedCheckoutPermissionExisted": "(es)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。"
}
},
"worktypeIdSetting": {
@@ -471,11 +483,11 @@
"addAccount": "Añadir cuenta",
"name": "Nombre de empresa",
"category": "Nivel de cuenta",
- "accountId": "ID de la cuenta",
+ "accountId": "ID de autor",
"country": "País",
"primaryAdmin": "Administrador primario",
"email": "Email",
- "dealerManagement": "Permitir que el concesionario realice los cambios",
+ "dealerManagement": "Permitir que el distribuidor realice los cambios",
"partners": "Socios",
"deleteAccount": "Borrar cuenta"
},
@@ -494,9 +506,9 @@
"accountID": "ID de la cuenta",
"yourCategory": "Tipo de cuenta",
"yourCountry": "País",
- "yourDealer": "Concesionario",
- "selectDealer": "Seleccionar Concesionario",
- "dealerManagement": "Permitir que el concesionario realice los cambios",
+ "yourDealer": "Distribuidor",
+ "selectDealer": "Seleccionar distribuidor",
+ "dealerManagement": "Permitir que el distribuidor realice los cambios",
"administratorInformation": "Información del administrador",
"primaryAdministrator": "Administrador primario",
"secondaryAdministrator": "Administrador secundario",
@@ -507,6 +519,9 @@
},
"message": {
"updateAccountFailedError": "No se pudo guardar la información de la cuenta. Actualice la pantalla e inténtelo de nuevo."
+ },
+ "text": {
+ "dealerManagementAnnotation": "Al habilitar la opción \"Permitir que el distribuidor realice los cambios\", usted acepta permitir que su distribuidor tenga derechos para acceder a su cuenta de ODMS Cloud para solicitar licencias y registrar usuarios en su nombre. Su distribuidor no tendrá acceso a ningún archivo de voz o documento almacenado en su cuenta de ODMS Cloud."
}
},
"deleteAccountPopup": {
@@ -538,22 +553,22 @@
},
"supportPage": {
"label": {
- "title": "(es)Support",
- "howToUse": "(es)How to use the system",
+ "title": "Soporte",
+ "howToUse": "Cómo utilizar el sistema",
"supportPageEnglish": "OMDS Cloud User Guide",
- "supportPageGerman": "OMDS Cloud Benutzerhandbuch",
- "supportPageFrench": "OMDS Cloud Mode d'emploi",
- "supportPageSpanish": "OMDS Cloud Guía del usuario"
+ "supportPageGerman": "OMDS Cloud-Benutzerhandbuch",
+ "supportPageFrench": "Guía del usuario de la nube OMDS",
+ "supportPageSpanish": "Guide de l'utilisateur du cloud OMDS"
},
"text": {
- "notResolved": "(es)If the problem persists even after referring to the user guide, please contact a higher-level person in charge."
+ "notResolved": "Consulte la Guía del usuario para obtener información sobre las funciones de ODMS Cloud. Si necesita soporte adicional, comuníquese con su administrador o revendedor certificado de ODMS Cloud."
}
},
"filePropertyPopup": {
"label": {
- "general": "(es)General",
- "job": "(es)Job",
- "close": "(es)Close"
+ "general": "General",
+ "job": "Trabajo",
+ "close": "Cerrar"
}
}
}
diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json
index 036d2ea..9cbdcd2 100644
--- a/dictation_client/src/translation/fr.json
+++ b/dictation_client/src/translation/fr.json
@@ -24,11 +24,11 @@
"headerDictations": "Dictées",
"headerWorkflow": "Flux de travail",
"headerPartners": "Partenaires",
- "headerSupport": "(fr)Support",
+ "headerSupport": "Support",
"tier1": "Admin",
"tier2": "BC",
"tier3": "Distributeur",
- "tier4": "Concessionnaire",
+ "tier4": "Revendeur",
"tier5": "Client",
"notSelected": "Aucune",
"signOutButton": "se déconnecter"
@@ -62,14 +62,14 @@
"title": "Créez votre compte",
"accountInfoTitle": "Information d'inscription",
"countryExplanation": "Sélectionnez le pays où vous vous trouvez. Si votre pays ne figure pas dans la liste, veuillez sélectionner le pays le plus proche.",
- "dealerExplanation": "Veuillez sélectionner le concessionnaire auprès duquel vous souhaitez acheter la licence.",
+ "dealerExplanation": "Veuillez sélectionner le revendeur auprès duquel vous souhaitez acheter la licence.",
"adminInfoTitle": "Enregistrer les informations de l'administrateur principal",
"passwordTerms": "Veuillez définir un mot de passe. Le mot de passe doit être composé de 8 à 25 caractères et doit contenir des lettres, des chiffres et des symboles. (Devrait lister les symboles compatibles et indiquer si une majuscule est nécessaire)."
},
"label": {
"company": "Nom de l'entreprise",
"country": "Pays",
- "dealer": "Concessionnaire (Facultatif)",
+ "dealer": "Revendeur (Facultatif)",
"adminName": "Nom de l'administrateur",
"email": "Adresse e-mail",
"password": "Mot de passe",
@@ -93,7 +93,7 @@
"label": {
"company": "Nom de l'entreprise",
"country": "Pays",
- "dealer": "Concessionnaire (Facultatif)",
+ "dealer": "Revendeur (Facultatif)",
"adminName": "Nom de l'administrateur",
"email": "Adresse e-mail",
"password": "Mot de passe",
@@ -128,7 +128,14 @@
"authorIdIncorrectError": "Le format de l'identifiant de l'auteur n'est pas valide. Seuls les caractères alphanumériques et \"_\" peuvent être saisis pour l'ID d'auteur.",
"roleChangeError": "Impossible de modifier le rôle de l'utilisateur. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.",
"encryptionPasswordCorrectError": "Le mot de passe de cryptage n'est pas conforme aux règles.",
- "alreadyLicenseDeallocatedError": "La licence attribuée a déjà été annulée. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut."
+ "alreadyLicenseDeallocatedError": "La licence attribuée a déjà été annulée. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.",
+ "UserDeletionLicenseActiveError": "(fr)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。",
+ "TypistDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。",
+ "AdminUserDeletionError": "(fr)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。",
+ "TypistUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。",
+ "AuthorUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。",
+ "TypistUserDeletionTranscriptionistGroupError": "(fr)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。",
+ "AuthorDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。"
},
"label": {
"title": "Utilisateur",
@@ -180,8 +187,8 @@
"storageSize": "Stockage disponible",
"usedSize": "Stockage utilisé",
"storageAvailable": "Stockage indisponible (montant dépassée)",
- "licenseLabel": "(fr)License",
- "storageLabel": "(fr)Storage"
+ "licenseLabel": "Licence",
+ "storageLabel": "Stockage"
}
},
"licenseOrderPage": {
@@ -190,7 +197,8 @@
"poNumberIncorrectError": "Le format du numéro de bon de commande n'est pas valide. Seuls des caractères alphanumériques peuvent être saisis pour le numéro de bon de commande.",
"newOrderIncorrectError": "Veuillez saisir un nombre supérieur ou égal à 1 pour la nouvelle commande.",
"confirmOrder": "Voulez-vous passer commande?",
- "poNumberConflictError": "Le numéro de bon de commande saisi existe déjà. Veuillez saisir un autre numéro de bon de commande."
+ "poNumberConflictError": "Le numéro de bon de commande saisi existe déjà. Veuillez saisir un autre numéro de bon de commande.",
+ "dealerNotFoundError": "(fr)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。"
},
"label": {
"title": "Commander licence",
@@ -206,8 +214,9 @@
"noPlaybackAuthorization": "Vous n'êtes pas autorisé à lire ce fichier.",
"taskToPlaybackNoExists": "Le fichier ne peut pas être lu car il a déjà été transcrit ou n'existe pas.",
"taskNotEditable": "Le transcripteur ne peut pas être changé car la transcription est déjà en cours ou le fichier n'existe pas. Veuillez actualiser l'écran et vérifier le dernier statut.",
- "backupFailedError": "(fr)ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。",
- "cancelFailedError": "(fr)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。"
+ "backupFailedError": "Le processus de « Sauvegarde de fichier » a échoué. Veuillez réessayer plus tard. Si l'erreur persiste, contactez votre administrateur système.",
+ "cancelFailedError": "Échec de la suppression des dictées. Veuillez actualiser votre écran et réessayer.",
+ "deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。"
},
"label": {
"title": "Dictées",
@@ -250,9 +259,9 @@
"deleteDictation": "Supprimer la dictée",
"selectedTranscriptionist": "Transcriptionniste sélectionné",
"poolTranscriptionist": "Liste de transcriptionniste",
- "fileBackup": "(fr)File Backup",
- "downloadForBackup": "(fr)Download for backup",
- "applications": "(fr)Applications",
+ "fileBackup": "Sauvegarde de fichiers",
+ "downloadForBackup": "Télécharger pour sauvegarde",
+ "applications": "Application de bureau",
"cancelDictation": "Annuler la transcription"
}
},
@@ -415,7 +424,10 @@
},
"message": {
"selectedTypistEmptyError": "Un ou plusieurs transcripteurs doivent être sélectionnés pour enregistrer un groupe de transcription.",
- "groupSaveFailedError": "Le groupe de dactylographes n'a pas pu être enregistré. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut."
+ "groupSaveFailedError": "Le groupe de transcriptionniste n'a pas pu être enregistré. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.",
+ "GroupNameAlreadyExistError": "(fr)このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。",
+ "deleteFailedWorkflowAssigned": "(fr)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。",
+ "deleteFailedCheckoutPermissionExisted": "(fr)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。"
}
},
"worktypeIdSetting": {
@@ -471,11 +483,11 @@
"addAccount": "Ajouter compte",
"name": "Nom de l'entreprise",
"category": "Niveau compte",
- "accountId": "identifiant de compte",
+ "accountId": "Identifiant Auteur",
"country": "Pays",
"primaryAdmin": "Administrateur principal",
"email": "Email",
- "dealerManagement": "Autoriser le concessionnaire à modifier les paramètres",
+ "dealerManagement": "Autoriser le revendeur à modifier les paramètres",
"partners": "Partenaires",
"deleteAccount": "Supprimer le compte"
},
@@ -494,9 +506,9 @@
"accountID": "identifiant de compte",
"yourCategory": "Type de compte",
"yourCountry": "Pays",
- "yourDealer": "Concessionnaire",
- "selectDealer": "Sélectionner le Concessionnaire",
- "dealerManagement": "Autoriser le concessionnaire à modifier les paramètres",
+ "yourDealer": "Revendeur",
+ "selectDealer": "Sélectionner le revendeur",
+ "dealerManagement": "Autoriser le revendeur à modifier les paramètres",
"administratorInformation": "Informations sur l'administrateur",
"primaryAdministrator": "Administrateur principal",
"secondaryAdministrator": "Administrateur secondaire",
@@ -507,6 +519,9 @@
},
"message": {
"updateAccountFailedError": "Échec de l'enregistrement des informations du compte. Veuillez actualiser l'écran et réessayer."
+ },
+ "text": {
+ "dealerManagementAnnotation": "En activant l'option « Autoriser le revendeur à modifier les paramètres », vous acceptez que votre concessionnaire ait les droits d'accès à votre compte ODMS Cloud pour commander des licences et enregistrer des utilisateurs en votre nom. Votre revendeur n'aura accès à aucun fichier(s) vocal(s) ou document(s) stocké(s) dans votre compte ODMS Cloud."
}
},
"deleteAccountPopup": {
@@ -538,22 +553,22 @@
},
"supportPage": {
"label": {
- "title": "(fr)Support",
- "howToUse": "(fr)How to use the system",
+ "title": "Support",
+ "howToUse": "Comment utiliser le système",
"supportPageEnglish": "OMDS Cloud User Guide",
- "supportPageGerman": "OMDS Cloud Benutzerhandbuch",
- "supportPageFrench": "OMDS Cloud Mode d'emploi",
- "supportPageSpanish": "OMDS Cloud Guía del usuario"
+ "supportPageGerman": "OMDS Cloud-Benutzerhandbuch",
+ "supportPageFrench": "Guía del usuario de la nube OMDS",
+ "supportPageSpanish": "Guide de l'utilisateur du cloud OMDS"
},
"text": {
- "notResolved": "(fr)If the problem persists even after referring to the user guide, please contact a higher-level person in charge."
+ "notResolved": "Veuillez vous référer au Guide de l'utilisateur pour plus d'informations sur les fonctionnalités d'ODMS Cloud. Si vous avez besoin d'une assistance supplémentaire, veuillez contacter votre administrateur ou votre revendeur certifié ODMS Cloud."
}
},
"filePropertyPopup": {
"label": {
- "general": "(fr)General",
- "job": "(fr)Job",
- "close": "(fr)Close"
+ "general": "Général",
+ "job": "Tâches",
+ "close": "Fermer"
}
}
}
diff --git a/dictation_server/.env.local.example b/dictation_server/.env.local.example
index c59d570..8c03436 100644
--- a/dictation_server/.env.local.example
+++ b/dictation_server/.env.local.example
@@ -16,7 +16,7 @@ MAIL_FROM=xxxxx@xxxxx.xxxx
NOTIFICATION_HUB_NAME=ntf-odms-dev
NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX
APP_DOMAIN=http://localhost:8081/
-STORAGE_TOKEN_EXPIRE_TIME=30
+STORAGE_TOKEN_EXPIRE_TIME=2
STORAGE_ACCOUNT_NAME_US=saodmsusdev
STORAGE_ACCOUNT_NAME_AU=saodmsaudev
STORAGE_ACCOUNT_NAME_EU=saodmseudev
@@ -26,10 +26,10 @@ STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX
STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA
STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA
STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA
-ACCESS_TOKEN_LIFETIME_WEB=7200000
-REFRESH_TOKEN_LIFETIME_WEB=86400000
-REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000
-EMAIL_CONFIRM_LIFETIME=86400000
+ACCESS_TOKEN_LIFETIME_WEB=7200
+REFRESH_TOKEN_LIFETIME_WEB=86400
+REFRESH_TOKEN_LIFETIME_DEFAULT=2592000
+EMAIL_CONFIRM_LIFETIME=86400
REDIS_HOST=redis-cache
REDIS_PORT=6379
REDIS_PASSWORD=omdsredispass
diff --git a/dictation_server/db/migrations/053-add_accounts_users_templates_index.sql b/dictation_server/db/migrations/053-add_accounts_users_templates_index.sql
new file mode 100644
index 0000000..5b4a4a6
--- /dev/null
+++ b/dictation_server/db/migrations/053-add_accounts_users_templates_index.sql
@@ -0,0 +1,19 @@
+-- +migrate Up
+ALTER TABLE `accounts` ADD INDEX `idx_accounts_tier` (tier);
+ALTER TABLE `accounts` ADD INDEX `idx_accounts_parent_account_id` (parent_account_id);
+ALTER TABLE `users` ADD INDEX `idx_users_external_id` (external_id);
+ALTER TABLE `users` ADD INDEX `idx_users_email_verified` (email_verified);
+ALTER TABLE `licenses` ADD INDEX `idx_licenses_order_id` (order_id);
+ALTER TABLE `licenses` ADD INDEX `idx_licenses_status` (status);
+ALTER TABLE `template_files` ADD INDEX `idx_template_files_account_id` (account_id);
+ALTER TABLE `template_files` ADD INDEX `idx_template_files_file_name` (file_name(500));
+
+-- +migrate Down
+ALTER TABLE `accounts` DROP INDEX `idx_accounts_tier`;
+ALTER TABLE `accounts` DROP INDEX `idx_accounts_parent_account_id`;
+ALTER TABLE `users` DROP INDEX `idx_users_external_id`;
+ALTER TABLE `users` DROP INDEX `idx_users_email_verified`;
+ALTER TABLE `licenses` DROP INDEX `idx_licenses_order_id`;
+ALTER TABLE `licenses` DROP INDEX `idx_licenses_status`;
+ALTER TABLE `template_files` DROP INDEX `idx_template_files_account_id`;
+ALTER TABLE `template_files` DROP INDEX `idx_template_files_file_name`;
\ No newline at end of file
diff --git a/dictation_server/db/migrations/054-add_license_index.sql b/dictation_server/db/migrations/054-add_license_index.sql
new file mode 100644
index 0000000..f08b2e9
--- /dev/null
+++ b/dictation_server/db/migrations/054-add_license_index.sql
@@ -0,0 +1,13 @@
+-- +migrate Up
+ALTER TABLE `license_orders` ADD INDEX `idx_po_number` (po_number);
+ALTER TABLE `license_orders` ADD INDEX `idx_from_account_id` (from_account_id);
+ALTER TABLE `license_orders` ADD INDEX `idx_status` (status);
+ALTER TABLE `card_licenses` ADD INDEX `idx_card_license_key` (card_license_key);
+ALTER TABLE `licenses` ADD INDEX `idx_status` (status);
+
+-- +migrate Down
+ALTER TABLE `license_orders` DROP INDEX `idx_po_number`;
+ALTER TABLE `license_orders` DROP INDEX `idx_from_account_id`;
+ALTER TABLE `license_orders` DROP INDEX `idx_status`;
+ALTER TABLE `card_licenses` DROP INDEX `idx_card_license_key`;
+ALTER TABLE `licenses` DROP INDEX `idx_status`;
\ No newline at end of file
diff --git a/dictation_server/db/migrations/055-add_users_index.sql b/dictation_server/db/migrations/055-add_users_index.sql
new file mode 100644
index 0000000..ea50c9d
--- /dev/null
+++ b/dictation_server/db/migrations/055-add_users_index.sql
@@ -0,0 +1,5 @@
+-- +migrate Up
+ALTER TABLE `users` ADD INDEX `idx_role` (role);
+
+-- +migrate Down
+ALTER TABLE `users` DROP INDEX `idx_role`;
\ No newline at end of file
diff --git a/dictation_server/db/migrations/056-add_tasks_index.sql b/dictation_server/db/migrations/056-add_tasks_index.sql
new file mode 100644
index 0000000..983ea67
--- /dev/null
+++ b/dictation_server/db/migrations/056-add_tasks_index.sql
@@ -0,0 +1,27 @@
+-- +migrate Up
+ALTER TABLE `tasks` ADD INDEX `idx_tasks_audio_file_id` (audio_file_id);
+ALTER TABLE `tasks` ADD INDEX `idx_tasks_status` (status);
+ALTER TABLE `tasks` ADD INDEX `idx_tasks_typist_user_id` (typist_user_id);
+ALTER TABLE `tasks` ADD INDEX `idx_tasks_is_job_number_enabled` (is_job_number_enabled);
+ALTER TABLE `checkout_permission` ADD INDEX `idx_checkout_permission_task_id` (task_id);
+ALTER TABLE `checkout_permission` ADD INDEX `idx_checkout_permission_user_group_id` (user_group_id);
+ALTER TABLE `checkout_permission` ADD INDEX `idx_checkout_permission_user_id` (user_id);
+ALTER TABLE `users` ADD INDEX `idx_users_role` (role);
+ALTER TABLE `users` ADD INDEX `idx_users_author_id` (author_id);
+ALTER TABLE `users` ADD INDEX `idx_users_deleted_at` (deleted_at);
+ALTER TABLE `worktypes` ADD INDEX `idx_worktypes_custom_worktype_id` (custom_worktype_id);
+ALTER TABLE `workflows` ADD INDEX `idx_workflows_account_id` (account_id);
+
+-- +migrate Down
+ALTER TABLE `tasks` DROP INDEX `idx_tasks_audio_file_id`;
+ALTER TABLE `tasks` DROP INDEX `idx_tasks_status`;
+ALTER TABLE `tasks` DROP INDEX `idx_tasks_typist_user_id`;
+ALTER TABLE `tasks` DROP INDEX `idx_tasks_is_job_number_enabled`;
+ALTER TABLE `checkout_permission` DROP INDEX `idx_checkout_permission_task_id`;
+ALTER TABLE `checkout_permission` DROP INDEX `idx_checkout_permission_user_group_id`;
+ALTER TABLE `checkout_permission` DROP INDEX `idx_checkout_permission_user_id`;
+ALTER TABLE `users` DROP INDEX `idx_users_role`;
+ALTER TABLE `users` DROP INDEX `idx_users_author_id`;
+ALTER TABLE `users` DROP INDEX `idx_users_deleted_at`;
+ALTER TABLE `worktypes` DROP INDEX `idx_worktypes_custom_worktype_id`;
+ALTER TABLE `workflows` DROP INDEX `idx_workflows_account_id`;
\ No newline at end of file
diff --git a/dictation_server/db/migrations/058-add_user_group_unique_constraint.sql b/dictation_server/db/migrations/058-add_user_group_unique_constraint.sql
new file mode 100644
index 0000000..c9cb2d6
--- /dev/null
+++ b/dictation_server/db/migrations/058-add_user_group_unique_constraint.sql
@@ -0,0 +1,6 @@
+-- +migrate Up
+ALTER TABLE `user_group` ADD UNIQUE `unique_index_account_id_name` (`account_id`, `name`);
+
+
+-- +migrate Down
+ALTER TABLE `user_group` DROP INDEX `unique_index_account_id_name`;
diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts
index 8f9dc2f..3c488da 100644
--- a/dictation_server/src/common/error/code.ts
+++ b/dictation_server/src/common/error/code.ts
@@ -59,6 +59,7 @@ export const ErrorCodes = [
'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
'E010812', // ライセンス未割当エラー
'E010908', // タイピストグループ不在エラー
+ 'E010909', // タイピストグループ名重複エラー
'E011001', // ワークタイプ重複エラー
'E011002', // ワークタイプ登録上限超過エラー
'E011003', // ワークタイプ不在エラー
diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts
index 0a27cc5..9383694 100644
--- a/dictation_server/src/common/error/message.ts
+++ b/dictation_server/src/common/error/message.ts
@@ -48,6 +48,7 @@ export const errors: Errors = {
E010811: 'Already license allocated Error',
E010812: 'License not allocated Error',
E010908: 'Typist Group not exist Error',
+ E010909: 'Typist Group name already exist Error',
E011001: 'This WorkTypeID already used Error',
E011002: 'WorkTypeID create limit exceeded Error',
E011003: 'WorkTypeID not found Error',
diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts
index 146d00a..ffaf05d 100644
--- a/dictation_server/src/common/test/overrides.ts
+++ b/dictation_server/src/common/test/overrides.ts
@@ -82,7 +82,8 @@ export const overrideSendgridService = (
overrides: {
sendMail?: (
context: Context,
- to: string,
+ to: string[],
+ cc: string[],
from: string,
subject: string,
text: string,
diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts
index 511e4b8..6657210 100644
--- a/dictation_server/src/constants/index.ts
+++ b/dictation_server/src/constants/index.ts
@@ -219,9 +219,9 @@ export const PNS = {
};
/**
- * ユーザーのライセンス状態
+ * ユーザーのライセンスの有効期限の状態
*/
-export const USER_LICENSE_STATUS = {
+export const USER_LICENSE_EXPIRY_STATUS = {
NORMAL: 'Normal',
NO_LICENSE: 'NoLicense',
ALERT: 'Alert',
@@ -311,3 +311,13 @@ export const USER_AUDIO_FORMAT = 'DS2(QP)';
* @const {string[]}
*/
export const NODE_ENV_TEST = 'test';
+
+/**
+ * ユーザに対するライセンスの状態
+ * @const {string[]}
+ */
+export const USER_LICENSE_STATUS = {
+ UNALLOCATED: 'unallocated',
+ ALLOCATED: 'allocated',
+ EXPIRED: 'expired',
+} as const;
diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts
index 4bfd687..d661635 100644
--- a/dictation_server/src/features/accounts/accounts.service.spec.ts
+++ b/dictation_server/src/features/accounts/accounts.service.spec.ts
@@ -134,7 +134,26 @@ describe('createAccount', () => {
},
});
- overrideSendgridService(service, {});
+ let _subject: string = "";
+ let _url: string | undefined = "";
+ overrideSendgridService(service, {
+ sendMail: async (
+ context: Context,
+ to: string[],
+ cc: string[],
+ from: string,
+ subject: string,
+ text: string,
+ html: string,
+ ) => {
+ const urlPattern = /https?:\/\/[^\s]+/g;
+ const urls = text.match(urlPattern);
+ const url = urls?.pop();
+
+ _subject = subject;
+ _url = url;
+ },
+ });
overrideBlobstorageService(service, {
createContainer: async () => {
return;
@@ -175,6 +194,10 @@ describe('createAccount', () => {
expect(user?.accepted_dpa_version).toBe(acceptedDpaVersion);
expect(user?.account_id).toBe(accountId);
expect(user?.role).toBe(role);
+
+ // 想定通りのメールが送られているか確認
+ expect(_subject).toBe('User Registration Notification [U-102]');
+ expect(_url?.startsWith('http://localhost:8081/mail-confirm?verify=')).toBeTruthy();
});
it('アカウントを作成がAzure AD B2Cへの通信失敗によって失敗すると500エラーが発生する', async () => {
@@ -5704,9 +5727,39 @@ describe('アカウント情報更新', () => {
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get(AccountsService);
+ let _subject: string = "";
+ let _url: string | undefined = "";
overrideSendgridService(service, {
- sendMail: async () => {
- return;
+ sendMail: async (
+ context: Context,
+ to: string[],
+ cc: string[],
+ from: string,
+ subject: string,
+ text: string,
+ html: string,
+ ) => {
+ const urlPattern = /https?:\/\/[^\s]+/g;
+ const urls = text.match(urlPattern);
+ const url = urls?.pop();
+
+ _subject = subject;
+ _url = url;
+ },
+ });
+ overrideAdB2cService(service, {
+ getUser: async (context, externalId) => {
+ return {
+ displayName: 'TEMP' + externalId,
+ id: externalId,
+ identities: [
+ {
+ signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
+ issuer: 'xxxxxx',
+ issuerAssignedId: 'mail@example.com',
+ },
+ ],
+ };
},
});
@@ -5733,6 +5786,9 @@ describe('アカウント情報更新', () => {
expect(account?.delegation_permission).toBe(true);
expect(account?.primary_admin_user_id).toBe(tier5Accounts.admin.id);
expect(account?.secondary_admin_user_id).toBe(null);
+ // 想定通りのメールが送られているか確認
+ expect(_subject).toBe('Account Edit Notification [U-112]');
+ expect(_url).toBe('http://localhost:8081/');
});
it('アカウント情報を更新する(第五階層以外が実行)', async () => {
if (!source) fail();
@@ -6364,7 +6420,27 @@ describe('deleteAccountAndData', () => {
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get(AccountsService);
- overrideSendgridService(service, {});
+ let _subject: string = '';
+ let _url: string | undefined = '';
+ overrideSendgridService(service, {
+ sendMail: async (
+ context: Context,
+ to: string[],
+ cc: string[],
+ from: string,
+ subject: string,
+ text: string,
+ html: string,
+ ) => {
+ const urlPattern = /https?:\/\/[^\s]+/g;
+ const urls = text.match(urlPattern);
+ const url = urls?.pop();
+
+ _subject = subject;
+ _url = url;
+ },
+ });
+
// 第一~第四階層のアカウント作成
const {
tier1Accounts: tier1Accounts,
@@ -6485,10 +6561,36 @@ describe('deleteAccountAndData', () => {
licensesB[0].id,
);
- // ADB2Cユーザーの削除成功
overrideAdB2cService(service, {
+ getUser: async (context, externalId) => {
+ return {
+ displayName: 'TEMP' + externalId,
+ id: externalId,
+ identities: [
+ {
+ signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
+ issuer: 'xxxxxx',
+ issuerAssignedId: 'mail@example.com',
+ },
+ ],
+ };
+ },
+ getUsers: async (context, externalIds) => {
+ return externalIds.map((x) => ({
+ displayName: 'admin',
+ id: x,
+ identities: [
+ {
+ signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
+ issuer: 'xxxxxx',
+ issuerAssignedId: `mail+${x}@example.com`,
+ },
+ ],
+ }));
+ },
deleteUsers: jest.fn(),
});
+
// blobstorageコンテナの削除成功
overrideBlobstorageService(service, {
deleteContainer: jest.fn(),
@@ -6559,6 +6661,9 @@ describe('deleteAccountAndData', () => {
const LicenseAllocationHistoryArchive =
await getLicenseAllocationHistoryArchive(source);
expect(LicenseAllocationHistoryArchive.length).toBe(1);
+
+ expect(_subject).toBe('Account Deleted Notification [U-111]');
+ expect(_url).toBe('http://localhost:8081/');
});
it('アカウントの削除に失敗した場合はエラーを返す', async () => {
if (!source) fail();
diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts
index 50273e6..8b6e271 100644
--- a/dictation_server/src/features/accounts/accounts.service.ts
+++ b/dictation_server/src/features/accounts/accounts.service.ts
@@ -60,6 +60,7 @@ import {
} from '../../repositories/licenses/errors/types';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import {
+ TypistGroupNameAlreadyExistError,
TypistGroupNotExistError,
TypistIdInvalidError,
} from '../../repositories/user_groups/errors/types';
@@ -1241,6 +1242,12 @@ export class AccountsService {
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
+ // 同名のタイピストグループが存在する場合は400エラーを返す
+ case TypistGroupNameAlreadyExistError:
+ throw new HttpException(
+ makeErrorResponse('E010909'),
+ HttpStatus.BAD_REQUEST,
+ );
default:
throw new HttpException(
makeErrorResponse('E009999'),
@@ -1315,6 +1322,12 @@ export class AccountsService {
makeErrorResponse('E010908'),
HttpStatus.BAD_REQUEST,
);
+ // 同名のタイピストグループが存在する場合は400エラーを返す
+ case TypistGroupNameAlreadyExistError:
+ throw new HttpException(
+ makeErrorResponse('E010909'),
+ HttpStatus.BAD_REQUEST,
+ );
default:
throw new HttpException(
makeErrorResponse('E009999'),
diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts
index e0202f7..2648785 100644
--- a/dictation_server/src/features/accounts/types/types.ts
+++ b/dictation_server/src/features/accounts/types/types.ts
@@ -198,7 +198,7 @@ export class CancelIssueRequest {
export class CreateWorktypesRequest {
@ApiProperty({ minLength: 1, maxLength: 255, description: 'WorktypeID' })
@MinLength(1)
- @MaxLength(255)
+ @MaxLength(16)
@IsRecorderAllowed()
worktypeId: string;
@ApiProperty({ description: 'Worktypeの説明', required: false })
@@ -210,7 +210,7 @@ export class CreateWorktypesRequest {
export class UpdateWorktypesRequest {
@ApiProperty({ minLength: 1, description: 'WorktypeID' })
@MinLength(1)
- @MaxLength(255)
+ @MaxLength(16)
@IsRecorderAllowed()
worktypeId: string;
@ApiProperty({ description: 'Worktypeの説明', required: false })
diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts
index 17d1253..deff777 100644
--- a/dictation_server/src/features/files/files.service.spec.ts
+++ b/dictation_server/src/features/files/files.service.spec.ts
@@ -235,67 +235,6 @@ describe('publishUploadSas', () => {
new HttpException(makeErrorResponse('E010812'), HttpStatus.BAD_REQUEST),
);
});
- it('アップロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => {
- if (!source) fail();
- // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する
- const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
- source,
- );
- const tier5Accounts = await makeTestAccount(source, {
- parent_account_id: tier4Accounts[0].account.id,
- tier: 5,
- });
- const {
- external_id: externalId,
- id: userId,
- author_id: authorId,
- } = await makeTestUser(source, {
- account_id: tier5Accounts.account.id,
- external_id: 'author-user-external-id',
- role: 'author',
- author_id: 'AUTHOR_ID',
- });
- // 昨日の日付を作成
- let yesterday = new Date();
- yesterday.setDate(yesterday.getDate() - 1);
- yesterday = new DateWithZeroTime(yesterday);
- // 期限切れのライセンスを作成して紐づける
- await createLicense(
- source,
- 1,
- yesterday,
- tier5Accounts.account.id,
- LICENSE_TYPE.NORMAL,
- LICENSE_ALLOCATED_STATUS.ALLOCATED,
- userId,
- null,
- null,
- null,
- );
- const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
-
- const blobParam = makeBlobstorageServiceMockValue();
- blobParam.publishUploadSas = `${url}?sas-token`;
- blobParam.fileExists = false;
-
- const notificationParam = makeDefaultNotificationhubServiceMockValue();
- const module = await makeTestingModuleWithBlobAndNotification(
- source,
- blobParam,
- notificationParam,
- );
- if (!module) fail();
- const service = module.get(FilesService);
-
- await expect(
- service.publishUploadSas(
- makeContext('trackingId', 'requestId'),
- externalId,
- ),
- ).rejects.toEqual(
- new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST),
- );
- });
});
describe('タスク作成から自動ルーティング(DB使用)', () => {
@@ -1097,76 +1036,6 @@ describe('音声ファイルダウンロードURL取得', () => {
),
).toEqual(`${url}?sas-token`);
});
- it('ダウンロードSASトークンが乗っているURLを取得できる(第五階層の場合ライセンスのチェックを行う)', async () => {
- if (!source) fail();
- // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する
- const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
- source,
- );
- const tier5Accounts = await makeTestAccount(source, {
- parent_account_id: tier4Accounts[0].account.id,
- tier: 5,
- });
- const {
- external_id: externalId,
- id: userId,
- author_id: authorId,
- } = await makeTestUser(source, {
- account_id: tier5Accounts.account.id,
- external_id: 'author-user-external-id',
- role: 'author',
- author_id: 'AUTHOR_ID',
- });
- // 本日の日付を作成
- let today = new Date();
- today.setDate(today.getDate());
- today = new DateWithZeroTime(today);
- // 有効期限内のライセンスを作成して紐づける
- await createLicense(
- source,
- 1,
- today,
- tier5Accounts.account.id,
- LICENSE_TYPE.NORMAL,
- LICENSE_ALLOCATED_STATUS.ALLOCATED,
- userId,
- null,
- null,
- null,
- );
- const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
-
- const { audioFileId } = await createTask(
- source,
- tier5Accounts.account.id,
- url,
- 'test.zip',
- 'InProgress',
- undefined,
- authorId ?? '',
- );
-
- const blobParam = makeBlobstorageServiceMockValue();
- blobParam.publishDownloadSas = `${url}?sas-token`;
- blobParam.fileExists = true;
-
- const notificationParam = makeDefaultNotificationhubServiceMockValue();
- const module = await makeTestingModuleWithBlobAndNotification(
- source,
- blobParam,
- notificationParam,
- );
- if (!module) fail();
- const service = module.get(FilesService);
-
- expect(
- await service.publishAudioFileDownloadSas(
- makeContext('trackingId', 'requestId'),
- externalId,
- audioFileId,
- ),
- ).toEqual(`${url}?sas-token`);
- });
it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
@@ -1396,133 +1265,6 @@ describe('音声ファイルダウンロードURL取得', () => {
new HttpException(makeErrorResponse('E010701'), HttpStatus.BAD_REQUEST),
);
});
- it('ダウンロード時にユーザーにライセンスが未割当の場合エラーとなる(第五階層限定)', async () => {
- if (!source) fail();
- // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する(ライセンスは作成しない)
- const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
- source,
- );
- const tier5Accounts = await makeTestAccount(source, {
- parent_account_id: tier4Accounts[0].account.id,
- tier: 5,
- });
- const {
- external_id: externalId,
- id: userId,
- author_id: authorId,
- } = await makeTestUser(source, {
- account_id: tier5Accounts.account.id,
- external_id: 'author-user-external-id',
- role: 'author',
- author_id: 'AUTHOR_ID',
- });
- const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
-
- const { audioFileId } = await createTask(
- source,
- tier5Accounts.account.id,
- url,
- 'test.zip',
- 'InProgress',
- undefined,
- authorId ?? '',
- );
-
- const blobParam = makeBlobstorageServiceMockValue();
- blobParam.publishDownloadSas = `${url}?sas-token`;
- blobParam.fileExists = false;
-
- const notificationParam = makeDefaultNotificationhubServiceMockValue();
- const module = await makeTestingModuleWithBlobAndNotification(
- source,
- blobParam,
- notificationParam,
- );
- if (!module) fail();
- const service = module.get(FilesService);
-
- await expect(
- service.publishAudioFileDownloadSas(
- makeContext('trackingId', 'requestId'),
- externalId,
- audioFileId,
- ),
- ).rejects.toEqual(
- new HttpException(makeErrorResponse('E010812'), HttpStatus.BAD_REQUEST),
- );
- });
- it('ダウンロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => {
- if (!source) fail();
- // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する
- const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
- source,
- );
- const tier5Accounts = await makeTestAccount(source, {
- parent_account_id: tier4Accounts[0].account.id,
- tier: 5,
- });
- const {
- external_id: externalId,
- id: userId,
- author_id: authorId,
- } = await makeTestUser(source, {
- account_id: tier5Accounts.account.id,
- external_id: 'author-user-external-id',
- role: 'author',
- author_id: 'AUTHOR_ID',
- });
- // 昨日の日付を作成
- let yesterday = new Date();
- yesterday.setDate(yesterday.getDate() - 1);
- yesterday = new DateWithZeroTime(yesterday);
- // 期限切れのライセンスを作成して紐づける
- await createLicense(
- source,
- 1,
- yesterday,
- tier5Accounts.account.id,
- LICENSE_TYPE.NORMAL,
- LICENSE_ALLOCATED_STATUS.ALLOCATED,
- userId,
- null,
- null,
- null,
- );
- const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
-
- const { audioFileId } = await createTask(
- source,
- tier5Accounts.account.id,
- url,
- 'test.zip',
- 'InProgress',
- undefined,
- authorId ?? '',
- );
-
- const blobParam = makeBlobstorageServiceMockValue();
- blobParam.publishDownloadSas = `${url}?sas-token`;
- blobParam.fileExists = false;
-
- const notificationParam = makeDefaultNotificationhubServiceMockValue();
- const module = await makeTestingModuleWithBlobAndNotification(
- source,
- blobParam,
- notificationParam,
- );
- if (!module) fail();
- const service = module.get(FilesService);
-
- await expect(
- service.publishAudioFileDownloadSas(
- makeContext('trackingId', 'requestId'),
- externalId,
- audioFileId,
- ),
- ).rejects.toEqual(
- new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST),
- );
- });
});
describe('テンプレートファイルダウンロードURL取得', () => {
@@ -1596,70 +1338,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
);
expect(resultUrl).toBe(`${url}?sas-token`);
});
- it('ダウンロードSASトークンが乗っているURLを取得できる(第五階層の場合ライセンスのチェックを行う)', async () => {
- if (!source) fail();
- // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する
- const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
- source,
- );
- const tier5Accounts = await makeTestAccount(source, {
- parent_account_id: tier4Accounts[0].account.id,
- tier: 5,
- });
- const { external_id: externalId, id: userId } = await makeTestUser(source, {
- account_id: tier5Accounts.account.id,
- external_id: 'typist-user-external-id',
- role: USER_ROLES.TYPIST,
- });
- // 本日の日付を作成
- let yesterday = new Date();
- yesterday.setDate(yesterday.getDate());
- yesterday = new DateWithZeroTime(yesterday);
- // 有効期限内のライセンスを作成して紐づける
- await createLicense(
- source,
- 1,
- yesterday,
- tier5Accounts.account.id,
- LICENSE_TYPE.NORMAL,
- LICENSE_ALLOCATED_STATUS.ALLOCATED,
- userId,
- null,
- null,
- null,
- );
- const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
- const { audioFileId } = await createTask(
- source,
- tier5Accounts.account.id,
- url,
- 'test.zip',
- TASK_STATUS.IN_PROGRESS,
- userId,
- 'AUTHOR_ID',
- );
-
- const blobParam = makeBlobstorageServiceMockValue();
- blobParam.publishDownloadSas = `${url}?sas-token`;
- blobParam.fileExists = true;
-
- const notificationParam = makeDefaultNotificationhubServiceMockValue();
- const module = await makeTestingModuleWithBlobAndNotification(
- source,
- blobParam,
- notificationParam,
- );
- if (!module) fail();
- const service = module.get(FilesService);
-
- const resultUrl = await service.publishTemplateFileDownloadSas(
- makeContext('trackingId', 'requestId'),
- externalId,
- audioFileId,
- );
- expect(resultUrl).toBe(`${url}?sas-token`);
- });
it('タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
@@ -1849,135 +1528,6 @@ describe('テンプレートファイルダウンロードURL取得', () => {
}
}
});
- it('ダウンロード時にユーザーにライセンスが未割当の場合エラーとなる(第五階層限定)', async () => {
- if (!source) fail();
- // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する(ライセンスは作成しない)
- const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
- source,
- );
- const tier5Accounts = await makeTestAccount(source, {
- parent_account_id: tier4Accounts[0].account.id,
- tier: 5,
- });
- const { external_id: externalId, id: userId } = await makeTestUser(source, {
- account_id: tier5Accounts.account.id,
- external_id: 'typist-user-external-id',
- role: USER_ROLES.TYPIST,
- });
- const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
-
- const { audioFileId } = await createTask(
- source,
- tier5Accounts.account.id,
- url,
- 'test.zip',
- TASK_STATUS.IN_PROGRESS,
- undefined,
- 'AUTHOR_ID',
- );
-
- const blobParam = makeBlobstorageServiceMockValue();
- blobParam.publishDownloadSas = `${url}?sas-token`;
- blobParam.fileExists = false;
-
- const notificationParam = makeDefaultNotificationhubServiceMockValue();
- const module = await makeTestingModuleWithBlobAndNotification(
- source,
- blobParam,
- notificationParam,
- );
- if (!module) fail();
- const service = module.get(FilesService);
-
- try {
- await service.publishTemplateFileDownloadSas(
- makeContext('trackingId', 'requestId'),
- externalId,
- audioFileId,
- );
- fail();
- } catch (e) {
- if (e instanceof HttpException) {
- expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
- expect(e.getResponse()).toEqual(makeErrorResponse('E010812'));
- } else {
- fail();
- }
- }
- });
- it('ダウンロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => {
- if (!source) fail();
- // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する
- const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts(
- source,
- );
- const tier5Accounts = await makeTestAccount(source, {
- parent_account_id: tier4Accounts[0].account.id,
- tier: 5,
- });
- const { external_id: externalId, id: userId } = await makeTestUser(source, {
- account_id: tier5Accounts.account.id,
- external_id: 'typist-user-external-id',
- role: USER_ROLES.TYPIST,
- });
- // 昨日の日付を作成
- let yesterday = new Date();
- yesterday.setDate(yesterday.getDate() - 1);
- yesterday = new DateWithZeroTime(yesterday);
- // 期限切れのライセンスを作成して紐づける
- await createLicense(
- source,
- 1,
- yesterday,
- tier5Accounts.account.id,
- LICENSE_TYPE.NORMAL,
- LICENSE_ALLOCATED_STATUS.ALLOCATED,
- userId,
- null,
- null,
- null,
- );
- const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`;
-
- const { audioFileId } = await createTask(
- source,
- tier5Accounts.account.id,
- url,
- 'test.zip',
- TASK_STATUS.IN_PROGRESS,
- undefined,
- 'AUTHOR_ID',
- );
-
- const blobParam = makeBlobstorageServiceMockValue();
- blobParam.publishDownloadSas = `${url}?sas-token`;
- blobParam.fileExists = false;
-
- const notificationParam = makeDefaultNotificationhubServiceMockValue();
- const module = await makeTestingModuleWithBlobAndNotification(
- source,
- blobParam,
- notificationParam,
- );
- if (!module) fail();
- const service = module.get(FilesService);
-
- try {
- await service.publishTemplateFileDownloadSas(
- makeContext('trackingId', 'requestId'),
- externalId,
- audioFileId,
- ),
- fail();
- } catch (e) {
- if (e instanceof HttpException) {
- expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
- expect(e.getResponse()).toEqual(makeErrorResponse('E010805'));
- } else {
- fail();
- }
- }
- });
});
describe('publishTemplateFileUploadSas', () => {
diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts
index 6c1b558..1f9c042 100644
--- a/dictation_server/src/features/files/files.service.ts
+++ b/dictation_server/src/features/files/files.service.ts
@@ -8,6 +8,7 @@ import {
OPTION_ITEM_NUM,
TASK_STATUS,
TIERS,
+ USER_LICENSE_STATUS,
USER_ROLES,
} from '../../constants/index';
import { User } from '../../repositories/users/entity/user.entity';
@@ -308,10 +309,10 @@ export class FilesService {
context,
user.id,
);
- if (state === 'expired') {
+ if (state === USER_LICENSE_STATUS.EXPIRED) {
throw new LicenseExpiredError('license is expired.');
}
- if (state === 'inallocated') {
+ if (state === USER_LICENSE_STATUS.UNALLOCATED) {
throw new LicenseNotAllocatedError('license is not allocated.');
}
}
@@ -392,20 +393,6 @@ export class FilesService {
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
- // 第五階層のみチェック
- if (user.account.tier === TIERS.TIER5) {
- // ライセンスが有効でない場合、エラー
- const { state } = await this.licensesRepository.getLicenseState(
- context,
- user.id,
- );
- if (state === 'expired') {
- throw new LicenseExpiredError('license is expired.');
- }
- if (state === 'inallocated') {
- throw new LicenseNotAllocatedError('license is not allocated.');
- }
- }
accountId = user.account.id;
userId = user.id;
country = user.account.country;
@@ -422,16 +409,6 @@ export class FilesService {
}`,
);
switch (e.constructor) {
- case LicenseExpiredError:
- throw new HttpException(
- makeErrorResponse('E010805'),
- HttpStatus.BAD_REQUEST,
- );
- case LicenseNotAllocatedError:
- throw new HttpException(
- makeErrorResponse('E010812'),
- HttpStatus.BAD_REQUEST,
- );
default:
throw new HttpException(
makeErrorResponse('E009999'),
@@ -571,20 +548,6 @@ export class FilesService {
if (!user.account) {
throw new AccountNotFoundError('account not found.');
}
- // 第五階層のみチェック
- if (user.account.tier === TIERS.TIER5) {
- // ライセンスが有効でない場合、エラー
- const { state } = await this.licensesRepository.getLicenseState(
- context,
- user.id,
- );
- if (state === 'expired') {
- throw new LicenseExpiredError('license is expired.');
- }
- if (state === 'inallocated') {
- throw new LicenseNotAllocatedError('license is not allocated.');
- }
- }
accountId = user.account_id;
userId = user.id;
country = user.account.country;
@@ -596,16 +559,6 @@ export class FilesService {
}`,
);
switch (e.constructor) {
- case LicenseExpiredError:
- throw new HttpException(
- makeErrorResponse('E010805'),
- HttpStatus.BAD_REQUEST,
- );
- case LicenseNotAllocatedError:
- throw new HttpException(
- makeErrorResponse('E010812'),
- HttpStatus.BAD_REQUEST,
- );
default:
throw new HttpException(
makeErrorResponse('E009999'),
diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts
index c3473ab..cdc56b2 100644
--- a/dictation_server/src/features/licenses/licenses.service.spec.ts
+++ b/dictation_server/src/features/licenses/licenses.service.spec.ts
@@ -17,15 +17,15 @@ import {
selectOrderLicense,
} from './test/utility';
import { UsersService } from '../users/users.service';
-import { makeContext } from '../../common/log';
-import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants';
+import { Context, makeContext } from '../../common/log';
+import { ADB2C_SIGN_IN_TYPE, LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants';
import {
makeHierarchicalAccounts,
makeTestSimpleAccount,
makeTestUser,
} from '../../common/test/utility';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
-import { overrideSendgridService } from '../../common/test/overrides';
+import { overrideAdB2cService, overrideSendgridService } from '../../common/test/overrides';
import { truncateAllTable } from '../../common/test/init';
describe('ライセンス注文', () => {
@@ -192,8 +192,8 @@ describe('ライセンス注文', () => {
await service.licenseOrders(context, externalId, poNumber, orderCount);
} catch (e) {
if (e instanceof HttpException) {
- expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
- expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
+ expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
+ expect(e.getResponse()).toEqual(makeErrorResponse('E010501'));
} else {
fail();
}
@@ -672,7 +672,18 @@ describe('ライセンス割り当て', () => {
const module = await makeTestingModule(source);
if (!module) fail();
- const { id: accountId } = await makeTestSimpleAccount(source);
+ const { id: dealerId } = await makeTestSimpleAccount(source, { company_name: "DEALER_COMPANY", tier: 4 });
+ const { id: dealerAdminId } = await makeTestUser(source, {
+ account_id: dealerId,
+ external_id: 'userId_admin',
+ role: 'admin',
+ author_id: undefined,
+ });
+
+ const { id: accountId } = await makeTestSimpleAccount(source, {
+ parent_account_id: dealerId,
+ tier: 5
+ });
const { id: userId } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
@@ -701,7 +712,55 @@ describe('ライセンス割り当て', () => {
);
const service = module.get(UsersService);
- overrideSendgridService(service, {});
+ let _subject: string = '';
+ let _url: string | undefined = '';
+ overrideAdB2cService(service, {
+ getUser: async (context, externalId) => {
+ return {
+ displayName: 'TEMP' + externalId,
+ id: externalId,
+ identities: [
+ {
+ signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
+ issuer: 'xxxxxx',
+ issuerAssignedId: 'mail@example.com',
+ },
+ ],
+ };
+ },
+ getUsers: async (context, externalIds) => {
+ return externalIds.map((x) => ({
+ displayName: 'admin',
+ id: x,
+ identities: [
+ {
+ signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
+ issuer: 'xxxxxx',
+ issuerAssignedId: `mail+${x}@example.com`,
+ },
+ ],
+ }));
+ }
+ });
+
+ overrideSendgridService(service, {
+ sendMail: async (
+ context: Context,
+ to: string[],
+ cc: string[],
+ from: string,
+ subject: string,
+ text: string,
+ html: string,
+ ) => {
+ const urlPattern = /https?:\/\/[^\s]+/g;
+ const urls = text.match(urlPattern);
+ const url = urls?.pop();
+
+ _subject = subject;
+ _url = url;
+ },
+ });
const expiry_date = new NewAllocatedLicenseExpirationDate();
@@ -735,6 +794,9 @@ describe('ライセンス割り当て', () => {
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
accountId,
);
+
+ expect(_subject).toBe('License Assigned Notification [U-108]');
+ expect(_url).toBe('http://localhost:8081/');
});
it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => {
diff --git a/dictation_server/src/features/licenses/licenses.service.ts b/dictation_server/src/features/licenses/licenses.service.ts
index 5b5ef19..3f64729 100644
--- a/dictation_server/src/features/licenses/licenses.service.ts
+++ b/dictation_server/src/features/licenses/licenses.service.ts
@@ -80,7 +80,9 @@ export class LicensesService {
.parent_account_id ?? undefined;
// 親アカウントIDが取得できない場合はエラー
if (parentAccountId === undefined) {
- throw new Error('parent account id is undefined');
+ throw new AccountNotFoundError(
+ `parent account id is not found. myAccountId: ${myAccountId}`,
+ );
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
@@ -147,6 +149,7 @@ export class LicensesService {
);
}
}
+
async issueCardLicenseKeys(
context: Context,
externalId: string,
diff --git a/dictation_server/src/features/tasks/tasks.module.ts b/dictation_server/src/features/tasks/tasks.module.ts
index c3e10e0..b95229b 100644
--- a/dictation_server/src/features/tasks/tasks.module.ts
+++ b/dictation_server/src/features/tasks/tasks.module.ts
@@ -8,6 +8,7 @@ import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_
import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module';
import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module';
import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module';
+import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module';
@Module({
imports: [
@@ -18,6 +19,7 @@ import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.r
AdB2cModule,
NotificationhubModule,
SendGridModule,
+ LicensesRepositoryModule,
],
providers: [TasksService],
controllers: [TasksController],
diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts
index 712f498..d50233a 100644
--- a/dictation_server/src/features/tasks/tasks.service.spec.ts
+++ b/dictation_server/src/features/tasks/tasks.service.spec.ts
@@ -25,7 +25,13 @@ import {
makeTestSimpleAccount,
makeTestUser,
} from '../../common/test/utility';
-import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
+import {
+ ADMIN_ROLES,
+ LICENSE_ALLOCATED_STATUS,
+ LICENSE_TYPE,
+ TASK_STATUS,
+ USER_ROLES,
+} from '../../constants';
import { makeTestingModule } from '../../common/test/modules';
import { createSortCriteria } from '../users/test/utility';
import { createWorktype } from '../accounts/test/utility';
@@ -38,6 +44,9 @@ import { NotificationhubService } from '../../gateways/notificationhub/notificat
import { Roles } from '../../common/types/role';
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
import { truncateAllTable } from '../../common/test/init';
+import { makeDefaultLicensesRepositoryMockValue } from '../accounts/test/accounts.service.mock';
+import { DateWithZeroTime } from '../licenses/types/types';
+import { createLicense } from '../licenses/test/utility';
describe('TasksService', () => {
it('タスク一覧を取得できる(admin)', async () => {
@@ -48,12 +57,15 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
+ const licensesRepositoryMockValue =
+ makeDefaultLicensesRepositoryMockValue();
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
+ licensesRepositoryMockValue,
);
const userId = 'userId';
@@ -122,6 +134,8 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
+ const licensesRepositoryMockValue =
+ makeDefaultLicensesRepositoryMockValue();
usersRepositoryMockValue.findUserByExternalId = new Error('DB failed');
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
@@ -129,6 +143,7 @@ describe('TasksService', () => {
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
+ licensesRepositoryMockValue,
);
const userId = 'userId';
@@ -164,6 +179,8 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
+ const licensesRepositoryMockValue =
+ makeDefaultLicensesRepositoryMockValue();
tasksRepositoryMockValue.getTasksFromAccountId = new Error('DB failed');
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
@@ -171,6 +188,7 @@ describe('TasksService', () => {
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
+ licensesRepositoryMockValue,
);
const userId = 'userId';
@@ -252,12 +270,15 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
+ const licensesRepositoryMockValue =
+ makeDefaultLicensesRepositoryMockValue();
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
+ licensesRepositoryMockValue,
);
const userId = 'userId';
const offset = 0;
@@ -292,6 +313,8 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
+ const licensesRepositoryMockValue =
+ makeDefaultLicensesRepositoryMockValue();
if (usersRepositoryMockValue.findUserByExternalId instanceof Error) {
return;
}
@@ -302,6 +325,7 @@ describe('TasksService', () => {
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
+ licensesRepositoryMockValue,
);
const userId = 'userId';
@@ -376,6 +400,8 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
+ const licensesRepositoryMockValue =
+ makeDefaultLicensesRepositoryMockValue();
tasksRepositoryMockValue.getTasksFromAuthorIdAndAccountId = new Error(
'DB failed',
);
@@ -385,6 +411,7 @@ describe('TasksService', () => {
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
+ licensesRepositoryMockValue,
);
const userId = 'userId';
@@ -420,6 +447,8 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
+ const licensesRepositoryMockValue =
+ makeDefaultLicensesRepositoryMockValue();
if (usersRepositoryMockValue.findUserByExternalId instanceof Error) {
return;
}
@@ -431,6 +460,7 @@ describe('TasksService', () => {
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
+ licensesRepositoryMockValue,
);
const userId = 'userId';
@@ -508,12 +538,15 @@ describe('TasksService', () => {
tasksRepositoryMockValue.getTasksFromTypistRelations = new Error(
'DB failed',
);
+ const licensesRepositoryMockValue =
+ makeDefaultLicensesRepositoryMockValue();
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
usersRepositoryMockValue,
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
+ licensesRepositoryMockValue,
);
const userId = 'userId';
@@ -549,6 +582,8 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
+ const licensesRepositoryMockValue =
+ makeDefaultLicensesRepositoryMockValue();
adb2cServiceMockValue.getUsers = new Adb2cTooManyRequestsError();
const service = await makeTasksServiceMock(
tasksRepositoryMockValue,
@@ -556,6 +591,7 @@ describe('TasksService', () => {
userGroupsRepositoryMockValue,
adb2cServiceMockValue,
notificationhubServiceMockValue,
+ licensesRepositoryMockValue,
);
const userId = 'userId';
@@ -1632,7 +1668,75 @@ describe('checkout', () => {
user_group_id: null,
});
});
+ it('第五階層のアカウントの場合、有効なライセンスが割当されている場合チェックアウトできる', async () => {
+ if (!source) fail();
+ const module = await makeTestingModule(source);
+ if (!module) fail();
+ // 第五階層のアカウントを作成
+ const { id: accountId } = await makeTestSimpleAccount(source, { tier: 5 });
+ const { id: typistUserId } = await makeTestUser(source, {
+ account_id: accountId,
+ external_id: 'typist-user-external-id',
+ role: 'typist',
+ });
+ const { id: authorUserId } = await makeTestUser(source, {
+ account_id: accountId,
+ external_id: 'author-user-external-id',
+ role: 'author',
+ author_id: 'MY_AUTHOR_ID',
+ });
+ // 本日の日付を作成
+ const today = new Date();
+ // 有効なライセンスを作成して紐づける
+ await createLicense(
+ source,
+ 1,
+ today,
+ accountId,
+ LICENSE_TYPE.NORMAL,
+ LICENSE_ALLOCATED_STATUS.ALLOCATED,
+ typistUserId,
+ null,
+ null,
+ null,
+ );
+ const { taskId } = await createTask(
+ source,
+ accountId,
+ authorUserId,
+ 'MY_AUTHOR_ID',
+ '',
+ '01',
+ '00000001',
+ 'Pending',
+ );
+ await createCheckoutPermissions(source, taskId, typistUserId);
+ const service = module.get(TasksService);
+
+ const initTask = await getTask(source, taskId);
+
+ await service.checkout(
+ makeContext('trackingId', 'requestId'),
+ 1,
+ ['typist'],
+ 'typist-user-external-id',
+ );
+ const resultTask = await getTask(source, taskId);
+ const permisions = await getCheckoutPermissions(source, taskId);
+
+ expect(resultTask?.status).toEqual('InProgress');
+ expect(resultTask?.typist_user_id).toEqual(typistUserId);
+ //タスクの元々のステータスがPending,Inprogressの場合、文字起こし開始時刻は更新されない
+ expect(resultTask?.started_at).toEqual(initTask?.started_at);
+ expect(permisions.length).toEqual(1);
+ expect(permisions[0]).toEqual({
+ id: 2,
+ task_id: 1,
+ user_id: 1,
+ user_group_id: null,
+ });
+ });
it('ユーザーのRoleがTypistで、対象のタスクのStatus[Uploaded,Inprogress,Pending]以外の時、タスクをチェックアウトできない', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
@@ -1678,7 +1782,116 @@ describe('checkout', () => {
}
}
});
+ it('第五階層のアカウントの場合、ライセンスが未割当の場合チェックアウトできない', async () => {
+ if (!source) fail();
+ const module = await makeTestingModule(source);
+ if (!module) fail();
+ // 第五階層のアカウントを作成
+ const { id: accountId } = await makeTestSimpleAccount(source, { tier: 5 });
+ await makeTestUser(source, {
+ account_id: accountId,
+ external_id: 'typist-user-external-id',
+ role: 'typist',
+ });
+ const { id: authorUserId } = await makeTestUser(source, {
+ account_id: accountId,
+ external_id: 'author-user-external-id',
+ role: 'author',
+ author_id: 'MY_AUTHOR_ID',
+ });
+ await createTask(
+ source,
+ accountId,
+ authorUserId,
+ 'MY_AUTHOR_ID',
+ '',
+ '01',
+ '00000001',
+ 'Backup',
+ );
+ const service = module.get(TasksService);
+ try {
+ await service.checkout(
+ makeContext('trackingId', 'requestId'),
+ 1,
+ ['typist'],
+ 'typist-user-external-id',
+ );
+ fail();
+ } catch (e) {
+ if (e instanceof HttpException) {
+ expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
+ expect(e.getResponse()).toEqual(makeErrorResponse('E010812'));
+ } else {
+ fail();
+ }
+ }
+ });
+ it('第五階層のアカウントの場合、ライセンスが有効期限切れの場合チェックアウトできない', async () => {
+ if (!source) fail();
+ const module = await makeTestingModule(source);
+ if (!module) fail();
+ // 第五階層のアカウントを作成
+ const { id: accountId } = await makeTestSimpleAccount(source, { tier: 5 });
+ const { id: typistUserId } = await makeTestUser(source, {
+ account_id: accountId,
+ external_id: 'typist-user-external-id',
+ role: 'typist',
+ });
+ const { id: authorUserId } = await makeTestUser(source, {
+ account_id: accountId,
+ external_id: 'author-user-external-id',
+ role: 'author',
+ author_id: 'MY_AUTHOR_ID',
+ });
+ // 昨日の日付を作成
+ let yesterday = new Date();
+ yesterday.setDate(yesterday.getDate() - 1);
+ yesterday = new DateWithZeroTime(yesterday);
+ // 期限切れのライセンスを作成して紐づける
+ await createLicense(
+ source,
+ 1,
+ yesterday,
+ accountId,
+ LICENSE_TYPE.NORMAL,
+ LICENSE_ALLOCATED_STATUS.ALLOCATED,
+ typistUserId,
+ null,
+ null,
+ null,
+ );
+
+ await createTask(
+ source,
+ accountId,
+ authorUserId,
+ 'MY_AUTHOR_ID',
+ '',
+ '01',
+ '00000001',
+ 'Backup',
+ );
+
+ const service = module.get(TasksService);
+ try {
+ await service.checkout(
+ makeContext('trackingId', 'requestId'),
+ 1,
+ ['typist'],
+ 'typist-user-external-id',
+ );
+ fail();
+ } catch (e) {
+ if (e instanceof HttpException) {
+ expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST);
+ expect(e.getResponse()).toEqual(makeErrorResponse('E010805'));
+ } else {
+ fail();
+ }
+ }
+ });
it('ユーザーのRoleがTypistで、チェックアウト権限が存在しない時、タスクをチェックアウトできない', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts
index d56055b..7b7c127 100644
--- a/dictation_server/src/features/tasks/tasks.service.ts
+++ b/dictation_server/src/features/tasks/tasks.service.ts
@@ -9,7 +9,13 @@ import {
SortDirection,
TaskListSortableAttribute,
} from '../../common/types/sort';
-import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
+import {
+ ADMIN_ROLES,
+ TASK_STATUS,
+ TIERS,
+ USER_LICENSE_STATUS,
+ USER_ROLES,
+} from '../../constants';
import {
AdB2cService,
Adb2cTooManyRequestsError,
@@ -36,6 +42,12 @@ import { User } from '../../repositories/users/entity/user.entity';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
+import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
+import {
+ LicenseExpiredError,
+ LicenseNotAllocatedError,
+} from '../../repositories/licenses/errors/types';
+import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
@Injectable()
export class TasksService {
@@ -48,6 +60,7 @@ export class TasksService {
private readonly adB2cService: AdB2cService,
private readonly sendgridService: SendGridService,
private readonly notificationhubService: NotificationhubService,
+ private readonly licensesRepository: LicensesRepositoryService,
) {}
async getTasks(
@@ -276,9 +289,26 @@ export class TasksService {
} | params: { audioFileId: ${audioFileId}, roles: ${roles}, externalId: ${externalId} };`,
);
- const { id, account_id, author_id } =
+ const { id, account_id, author_id, account } =
await this.usersRepository.findUserByExternalId(context, externalId);
+ if (!account) {
+ throw new AccountNotFoundError('account not found.');
+ }
+ // 第五階層のみチェック
+ if (account.tier === TIERS.TIER5) {
+ // ライセンスが有効でない場合、エラー
+ const { state } = await this.licensesRepository.getLicenseState(
+ context,
+ id,
+ );
+ if (state === USER_LICENSE_STATUS.EXPIRED) {
+ throw new LicenseExpiredError('license is expired.');
+ }
+ if (state === USER_LICENSE_STATUS.UNALLOCATED) {
+ throw new LicenseNotAllocatedError('license is not allocated.');
+ }
+ }
if (roles.includes(USER_ROLES.AUTHOR)) {
// API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
if (!author_id) {
@@ -308,6 +338,16 @@ export class TasksService {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
+ case LicenseExpiredError:
+ throw new HttpException(
+ makeErrorResponse('E010805'),
+ HttpStatus.BAD_REQUEST,
+ );
+ case LicenseNotAllocatedError:
+ throw new HttpException(
+ makeErrorResponse('E010812'),
+ HttpStatus.BAD_REQUEST,
+ );
case CheckoutPermissionNotFoundError:
case TaskAuthorIdNotMatchError:
case InvalidRoleError:
diff --git a/dictation_server/src/features/tasks/test/tasks.service.mock.ts b/dictation_server/src/features/tasks/test/tasks.service.mock.ts
index 70dc66a..024f763 100644
--- a/dictation_server/src/features/tasks/test/tasks.service.mock.ts
+++ b/dictation_server/src/features/tasks/test/tasks.service.mock.ts
@@ -17,6 +17,11 @@ import { NotificationhubService } from '../../../gateways/notificationhub/notifi
import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service';
import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service';
import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service';
+import {
+ LicensesRepositoryMockValue,
+ makeLicensesRepositoryMock,
+} from '../../accounts/test/accounts.service.mock';
+import { LicensesRepositoryService } from '../../../repositories/licenses/licenses.repository.service';
export type TasksRepositoryMockValue = {
getTasksFromAccountId:
@@ -65,6 +70,7 @@ export const makeTasksServiceMock = async (
userGroupsRepositoryMockValue: UserGroupsRepositoryMockValue,
adB2CServiceMockValue: AdB2CServiceMockValue,
notificationhubServiceMockValue: NotificationhubServiceMockValue,
+ licensesRepositoryMockValue: LicensesRepositoryMockValue,
): Promise<{
tasksService: TasksService;
taskRepoService: TasksRepositoryService;
@@ -92,6 +98,8 @@ export const makeTasksServiceMock = async (
// メール送信でしか利用しておらず、テストする必要がないが、依存関係解決のため空オブジェクトを定義しておく。
case SendGridService:
return {};
+ case LicensesRepositoryService:
+ return makeLicensesRepositoryMock(licensesRepositoryMockValue);
}
})
.compile();
diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts
index 80b1a13..7cd4e44 100644
--- a/dictation_server/src/features/users/types/types.ts
+++ b/dictation_server/src/features/users/types/types.ts
@@ -9,7 +9,7 @@ import {
} from 'class-validator';
import {
TASK_LIST_SORTABLE_ATTRIBUTES,
- USER_LICENSE_STATUS,
+ USER_LICENSE_EXPIRY_STATUS,
} from '../../../constants';
import { USER_ROLES } from '../../../constants';
import {
@@ -67,9 +67,9 @@ export class User {
remaining?: number;
@ApiProperty({
- description: `${Object.values(USER_LICENSE_STATUS).join('/')}`,
+ description: `${Object.values(USER_LICENSE_EXPIRY_STATUS).join('/')}`,
})
- @IsIn(Object.values(USER_LICENSE_STATUS), {
+ @IsIn(Object.values(USER_LICENSE_EXPIRY_STATUS), {
message: 'invalid license status',
})
licenseStatus: string;
diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts
index 8e4a9fe..8e3cbf5 100644
--- a/dictation_server/src/features/users/users.service.spec.ts
+++ b/dictation_server/src/features/users/users.service.spec.ts
@@ -22,7 +22,7 @@ import {
LICENSE_EXPIRATION_THRESHOLD_DAYS,
LICENSE_TYPE,
USER_AUDIO_FORMAT,
- USER_LICENSE_STATUS,
+ USER_LICENSE_EXPIRY_STATUS,
USER_ROLES,
} from '../../constants';
import { makeTestingModule } from '../../common/test/modules';
@@ -108,7 +108,26 @@ describe('UsersService.confirmUser', () => {
});
const service = module.get(UsersService);
- overrideSendgridService(service, {});
+ let _subject: string = '';
+ let _url: string | undefined = '';
+ overrideSendgridService(service, {
+ sendMail: async (
+ context: Context,
+ to: string[],
+ cc: string[],
+ from: string,
+ subject: string,
+ text: string,
+ html: string,
+ ) => {
+ const urlPattern = /https?:\/\/[^\s]+/g;
+ const urls = text.match(urlPattern);
+ const url = urls?.pop();
+
+ _subject = subject;
+ _url = url;
+ },
+ });
// account id:1, user id: 2のトークン
const token =
@@ -149,6 +168,8 @@ describe('UsersService.confirmUser', () => {
delete_order_id: null,
user: null,
});
+ expect(_subject).toBe('Account Registered Notification [U-101]');
+ expect(_url).toBe('http://localhost:8081/');
}, 600000);
it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => {
@@ -506,7 +527,26 @@ describe('UsersService.createUser', () => {
};
},
});
- overrideSendgridService(service, {});
+ let _subject: string = '';
+ let _url: string | undefined = '';
+ overrideSendgridService(service, {
+ sendMail: async (
+ context: Context,
+ to: string[],
+ cc: string[],
+ from: string,
+ subject: string,
+ text: string,
+ html: string,
+ ) => {
+ const urlPattern = /https?:\/\/[^\s]+/g;
+ const urls = text.match(urlPattern);
+ const url = urls?.pop();
+
+ _subject = subject;
+ _url = url;
+ },
+ });
expect(
await service.createUser(
@@ -536,6 +576,11 @@ describe('UsersService.createUser', () => {
// 他にユーザーが登録されていないことを確認
const users = await getUsers(source);
expect(users.length).toEqual(2);
+
+ expect(_subject).toBe('User Registration Notification [U-114]');
+ expect(
+ _url?.startsWith('http://localhost:8081/mail-confirm/user?verify='),
+ ).toBeTruthy();
});
it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化あり)', async () => {
@@ -1479,7 +1524,7 @@ describe('UsersService.getUsers', () => {
prompt: false,
expiration: undefined,
remaining: undefined,
- licenseStatus: USER_LICENSE_STATUS.NO_LICENSE,
+ licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
},
{
id: typistUserId,
@@ -1495,7 +1540,7 @@ describe('UsersService.getUsers', () => {
prompt: false,
expiration: undefined,
remaining: undefined,
- licenseStatus: USER_LICENSE_STATUS.NO_LICENSE,
+ licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
},
{
id: noneUserId,
@@ -1511,7 +1556,7 @@ describe('UsersService.getUsers', () => {
prompt: false,
expiration: undefined,
remaining: undefined,
- licenseStatus: USER_LICENSE_STATUS.NO_LICENSE,
+ licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
},
];
@@ -1591,7 +1636,7 @@ describe('UsersService.getUsers', () => {
date1.getMonth() + 1
}/${date1.getDate()}`,
remaining: LICENSE_EXPIRATION_THRESHOLD_DAYS + 1,
- licenseStatus: USER_LICENSE_STATUS.NORMAL,
+ licenseStatus: USER_LICENSE_EXPIRY_STATUS.NORMAL,
},
{
id: user2,
@@ -1609,7 +1654,7 @@ describe('UsersService.getUsers', () => {
date2.getMonth() + 1
}/${date2.getDate()}`,
remaining: LICENSE_EXPIRATION_THRESHOLD_DAYS,
- licenseStatus: USER_LICENSE_STATUS.RENEW,
+ licenseStatus: USER_LICENSE_EXPIRY_STATUS.RENEW,
},
{
id: user3,
@@ -1627,7 +1672,7 @@ describe('UsersService.getUsers', () => {
date3.getMonth() + 1
}/${date3.getDate()}`,
remaining: LICENSE_EXPIRATION_THRESHOLD_DAYS - 1,
- licenseStatus: USER_LICENSE_STATUS.ALERT,
+ licenseStatus: USER_LICENSE_EXPIRY_STATUS.ALERT,
},
];
diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts
index e48e24f..359f163 100644
--- a/dictation_server/src/features/users/users.service.ts
+++ b/dictation_server/src/features/users/users.service.ts
@@ -37,7 +37,7 @@ import {
MANUAL_RECOVERY_REQUIRED,
OPTION_ITEM_VALUE_TYPE_NUMBER,
USER_AUDIO_FORMAT,
- USER_LICENSE_STATUS,
+ USER_LICENSE_EXPIRY_STATUS,
USER_ROLES,
} from '../../constants';
import { DateWithZeroTime } from '../licenses/types/types';
@@ -617,7 +617,7 @@ export class UsersService {
throw new Error('mail not found.');
}
- let status = USER_LICENSE_STATUS.NORMAL;
+ let status = USER_LICENSE_EXPIRY_STATUS.NORMAL;
// ライセンスの有効期限と残日数は、ライセンスが存在する場合のみ算出する
// ライセンスが存在しない場合は、undefinedのままとする
@@ -648,11 +648,11 @@ export class UsersService {
remaining <= LICENSE_EXPIRATION_THRESHOLD_DAYS
) {
status = dbUser.auto_renew
- ? USER_LICENSE_STATUS.RENEW
- : USER_LICENSE_STATUS.ALERT;
+ ? USER_LICENSE_EXPIRY_STATUS.RENEW
+ : USER_LICENSE_EXPIRY_STATUS.ALERT;
}
} else {
- status = USER_LICENSE_STATUS.NO_LICENSE;
+ status = USER_LICENSE_EXPIRY_STATUS.NO_LICENSE;
}
return {
diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts
index 6e2fb15..985c7b1 100644
--- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts
+++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts
@@ -21,6 +21,7 @@ import {
VERIFY_LINK,
TEMPORARY_PASSWORD,
} from '../../templates/constants';
+import { URL } from 'node:url';
@Injectable()
export class SendGridService {
@@ -204,12 +205,13 @@ export class SendGridService {
);
try {
const subject = 'Account Registered Notification [U-101]';
+ const url = new URL(this.appDomain).href;
const html = this.templateU101Html
.replaceAll(CUSTOMER_NAME, customerAccountName)
- .replaceAll(TOP_URL, this.appDomain);
+ .replaceAll(TOP_URL, url);
const text = this.templateU101Text
.replaceAll(CUSTOMER_NAME, customerAccountName)
- .replaceAll(TOP_URL, this.appDomain);
+ .replaceAll(TOP_URL, url);
await this.sendMail(
context,
@@ -255,8 +257,9 @@ export class SendGridService {
this.emailConfirmLifetime,
privateKey,
);
- const path = 'mail-confirm/';
- const verifyUrl = `${this.appDomain}${path}?verify=${token}`;
+ const paths = path.join('mail-confirm');
+ const url = new URL(paths, this.appDomain).href;
+ const verifyUrl = `${url}?verify=${token}`;
const subject = 'User Registration Notification [U-102]';
const html = this.templateU102Html.replaceAll(VERIFY_LINK, verifyUrl);
@@ -466,6 +469,7 @@ export class SendGridService {
);
try {
const subject = 'License Assigned Notification [U-108]';
+ const url = new URL(this.appDomain).href;
// メールの本文を作成する
const html = this.templateU108Html
@@ -473,13 +477,13 @@ export class SendGridService {
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
- .replaceAll(TOP_URL, this.appDomain);
+ .replaceAll(TOP_URL, url);
const text = this.templateU108Text
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
- .replaceAll(TOP_URL, this.appDomain);
+ .replaceAll(TOP_URL, url);
const ccAddress = customerAdminMails.includes(userMail) ? [] : [userMail];
@@ -574,17 +578,18 @@ export class SendGridService {
);
try {
const subject = 'Account Deleted Notification [U-111]';
+ const url = new URL(this.appDomain).href;
// メールの本文を作成する
const html = this.templateU111Html
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
- .replaceAll(TOP_URL, this.appDomain);
+ .replaceAll(TOP_URL, url);
const text = this.templateU111Text
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
- .replaceAll(TOP_URL, this.appDomain);
+ .replaceAll(TOP_URL, url);
// メールを送信する
await this.sendMail(
@@ -627,6 +632,7 @@ export class SendGridService {
let html: string;
let text: string;
+ const url = new URL(this.appDomain).href;
// 親アカウントがない場合は別のテンプレートを使用する
if (dealerAccountName === null) {
@@ -634,22 +640,22 @@ export class SendGridService {
html = this.templateU112NoParentHtml
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
- .replaceAll(TOP_URL, this.appDomain);
+ .replaceAll(TOP_URL, url);
text = this.templateU112NoParentText
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
- .replaceAll(TOP_URL, this.appDomain);
+ .replaceAll(TOP_URL, url);
} else {
html = this.templateU112Html
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
- .replaceAll(TOP_URL, this.appDomain);
+ .replaceAll(TOP_URL, url);
text = this.templateU112Text
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
- .replaceAll(TOP_URL, this.appDomain);
+ .replaceAll(TOP_URL, url);
}
// メールを送信する
@@ -745,18 +751,20 @@ export class SendGridService {
this.emailConfirmLifetime,
privateKey,
);
- const path = 'mail-confirm/user/';
- const verifyLink = `${this.appDomain}${path}?verify=${token}`;
+
+ const paths = path.join('mail-confirm', '/user');
+ const url = new URL(paths, this.appDomain);
+ const verifyUrl = `${url}?verify=${token}`;
const subject = 'User Registration Notification [U-114]';
// メールの本文を作成する
const html = this.templateU114Html
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
- .replaceAll(VERIFY_LINK, verifyLink);
+ .replaceAll(VERIFY_LINK, verifyUrl);
const text = this.templateU114Text
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
- .replaceAll(VERIFY_LINK, verifyLink);
+ .replaceAll(VERIFY_LINK, verifyUrl);
// メールを送信する
await this.sendMail(
diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts
index 6d7c758..2832672 100644
--- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts
+++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts
@@ -821,6 +821,7 @@ export class AccountsRepositoryService {
status: Not(LICENSE_ALLOCATED_STATUS.UNALLOCATED),
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// 存在した場合エラー
@@ -1023,6 +1024,7 @@ export class AccountsRepositoryService {
email_verified: true,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!primaryAdminUser) {
throw new AdminUserNotFoundError(
@@ -1040,6 +1042,7 @@ export class AccountsRepositoryService {
email_verified: true,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!secondryAdminUser) {
throw new AdminUserNotFoundError(
diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts
index 9f9cb85..6270b43 100644
--- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts
+++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts
@@ -12,9 +12,9 @@ import {
LICENSE_ALLOCATED_STATUS,
LICENSE_ISSUE_STATUS,
LICENSE_TYPE,
- NODE_ENV_TEST,
SWITCH_FROM_TYPE,
TIERS,
+ USER_LICENSE_STATUS,
} from '../../constants';
import {
PoNumberAlreadyExistError,
@@ -39,6 +39,8 @@ import {
updateEntity,
} from '../../common/repository';
import { Context } from '../../common/log';
+import { User } from '../users/entity/user.entity';
+import { UserNotFoundError } from '../users/errors/types';
@Injectable()
export class LicensesRepositoryService {
@@ -81,6 +83,7 @@ export class LicensesRepositoryService {
},
],
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// 重複があった場合はエラーを返却する
if (isPoNumberDuplicated) {
@@ -193,6 +196,7 @@ export class LicensesRepositoryService {
card_license_key: In(generateKeys),
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (existingCardLicenses.length > 0) {
// 重複分を配列から削除
@@ -292,6 +296,7 @@ export class LicensesRepositoryService {
card_license_key: licenseKey,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// カードライセンスが存在しなければエラー
if (!targetCardLicense) {
@@ -422,10 +427,7 @@ export class LicensesRepositoryService {
po_number: poNumber,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
- // テスト環境の場合はロックを行わない(sqliteがlockに対応していないため)
- ...(process.env.NODE_ENV !== NODE_ENV_TEST
- ? { lock: { mode: 'pessimistic_write' } }
- : {}),
+ lock: { mode: 'pessimistic_write' },
});
if (!issuingOrder) {
// 注文が存在しない場合、エラー
@@ -559,6 +561,19 @@ export class LicensesRepositoryService {
accountId: number,
): Promise {
await this.dataSource.transaction(async (entityManager) => {
+ // 対象ユーザの存在チェック
+ const userRepo = entityManager.getRepository(User);
+ const user = await userRepo.findOne({
+ where: {
+ id: userId,
+ },
+ comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
+ });
+ if (!user) {
+ throw new UserNotFoundError(`User not exist. userId: ${userId}`);
+ }
+
const licenseRepo = entityManager.getRepository(License);
const licenseAllocationHistoryRepo = entityManager.getRepository(
LicenseAllocationHistory,
@@ -569,6 +584,7 @@ export class LicensesRepositoryService {
id: newLicenseId,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// ライセンスが存在しない場合はエラー
@@ -604,6 +620,7 @@ export class LicensesRepositoryService {
allocated_user_id: userId,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// 既にライセンスが割り当てられているなら、割り当てを解除
@@ -719,6 +736,7 @@ export class LicensesRepositoryService {
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// ライセンスが割り当てられていない場合はエラー
@@ -778,6 +796,7 @@ export class LicensesRepositoryService {
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// キャンセル対象の注文が存在しない場合エラー
@@ -806,12 +825,17 @@ export class LicensesRepositoryService {
* ライセンスの割当状態を取得します
* @param userId ユーザーID
* @error { Error } DBアクセス失敗時の例外
- * @returns Promise<{ state: 'allocated' | 'inallocated' | 'expired' }>
+ * @returns Promise<{ state: 'allocated' | 'unallocated' | 'expired' }>
*/
async getLicenseState(
context: Context,
userId: number,
- ): Promise<{ state: 'allocated' | 'inallocated' | 'expired' }> {
+ ): Promise<{
+ state:
+ | typeof USER_LICENSE_STATUS.ALLOCATED
+ | typeof USER_LICENSE_STATUS.UNALLOCATED
+ | typeof USER_LICENSE_STATUS.EXPIRED;
+ }> {
const allocatedLicense = await this.dataSource
.getRepository(License)
.findOne({
@@ -824,7 +848,7 @@ export class LicensesRepositoryService {
// ライセンスが割り当てられていない場合は未割当状態
if (allocatedLicense == null) {
- return { state: 'inallocated' };
+ return { state: USER_LICENSE_STATUS.UNALLOCATED };
}
// ライセンスの有効期限が過ぎている場合は期限切れ状態
@@ -833,9 +857,9 @@ export class LicensesRepositoryService {
allocatedLicense.expiry_date &&
allocatedLicense.expiry_date < currentDate
) {
- return { state: 'expired' };
+ return { state: USER_LICENSE_STATUS.EXPIRED };
}
- return { state: 'allocated' };
+ return { state: USER_LICENSE_STATUS.ALLOCATED };
}
}
diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts
index 9ac0e48..a95a8ab 100644
--- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts
+++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts
@@ -48,6 +48,7 @@ import {
deleteEntity,
} from '../../common/repository';
import { Context } from '../../common/log';
+import { UserNotFoundError } from '../users/errors/types';
@Injectable()
export class TasksRepositoryService {
@@ -167,6 +168,20 @@ export class TasksRepositoryService {
permittedSourceStatus: TaskStatus[],
): Promise {
await this.dataSource.transaction(async (entityManager) => {
+ // 対象ユーザの存在確認
+ const userRepo = entityManager.getRepository(User);
+ const user = await userRepo.findOne({
+ where: {
+ id: user_id,
+ },
+ comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
+ });
+ if (!user) {
+ throw new TypistUserNotFoundError(
+ `Typist user not exists. user_id:${user_id}`,
+ );
+ }
const taskRepo = entityManager.getRepository(Task);
// 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得
const task = await taskRepo.findOne({
@@ -174,6 +189,7 @@ export class TasksRepositoryService {
audio_file_id: audio_file_id,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!task) {
throw new TasksNotFoundError(
@@ -189,6 +205,7 @@ export class TasksRepositoryService {
typist_user_id: user_id,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (tasks.length > 0) {
@@ -227,6 +244,7 @@ export class TasksRepositoryService {
},
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// ユーザーの所属するすべてのグループIDを列挙
const groupIds = groups.map((member) => member.user_group_id);
@@ -249,6 +267,7 @@ export class TasksRepositoryService {
},
],
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
//チェックアウト権限がなければエラー
@@ -319,6 +338,7 @@ export class TasksRepositoryService {
audio_file_id: audio_file_id,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!task) {
throw new TasksNotFoundError(
@@ -372,6 +392,7 @@ export class TasksRepositoryService {
audio_file_id: audio_file_id,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!task) {
throw new TasksNotFoundError(
@@ -447,6 +468,7 @@ export class TasksRepositoryService {
audio_file_id: audio_file_id,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!task) {
throw new TasksNotFoundError(
@@ -498,6 +520,7 @@ export class TasksRepositoryService {
audio_file_id: audio_file_id,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!task) {
throw new TasksNotFoundError(
@@ -846,6 +869,22 @@ export class TasksRepositoryService {
const createdEntity = await this.dataSource.transaction(
async (entityManager) => {
+ // タスクの所有者の存在確認
+ const userRepo = entityManager.getRepository(User);
+ const user = await userRepo.findOne({
+ where: {
+ id: owner_user_id,
+ account_id: account_id,
+ },
+ comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
+ });
+ if (!user) {
+ throw new UserNotFoundError(
+ `User not exists. owner_user_id:${owner_user_id}`,
+ );
+ }
+
const audioFileRepo = entityManager.getRepository(AudioFile);
const newAudioFile = audioFileRepo.create(audioFile);
const savedAudioFile = await insertEntity(
@@ -865,6 +904,7 @@ export class TasksRepositoryService {
where: { account_id: account_id, is_job_number_enabled: true },
order: { created_at: 'DESC', job_number: 'DESC' },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
let newJobNumber = '00000001';
@@ -927,30 +967,6 @@ export class TasksRepositoryService {
assignees: Assignee[],
): Promise {
await this.dataSource.transaction(async (entityManager) => {
- // UserGroupの取得/存在確認
- const userGroupIds = assignees
- .filter((x) => x.typistGroupId !== undefined)
- .map((y) => {
- return y.typistGroupId;
- });
- const groupRepo = entityManager.getRepository(UserGroup);
- const groupRecords = await groupRepo.find({
- where: {
- id: In(userGroupIds),
- account_id: account_id,
- deleted_at: IsNull(),
- },
- comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
- });
- // idはユニークであるため取得件数の一致でグループの存在を確認
- if (userGroupIds.length !== groupRecords.length) {
- throw new TypistUserGroupNotFoundError(
- `Group not exists Error. reqUserGroupId:${userGroupIds}; resUserGroupId:${groupRecords.map(
- (x) => x.id,
- )}`,
- );
- }
-
// Userの取得/存在確認
const typistUserIds = assignees
.filter((x) => x.typistUserId !== undefined)
@@ -967,6 +983,7 @@ export class TasksRepositoryService {
deleted_at: IsNull(),
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// idはユニークであるため取得件数の一致でユーザーの存在を確認
if (typistUserIds.length !== userRecords.length) {
@@ -977,6 +994,31 @@ export class TasksRepositoryService {
);
}
+ // UserGroupの取得/存在確認
+ const userGroupIds = assignees
+ .filter((x) => x.typistGroupId !== undefined)
+ .map((y) => {
+ return y.typistGroupId;
+ });
+ const groupRepo = entityManager.getRepository(UserGroup);
+ const groupRecords = await groupRepo.find({
+ where: {
+ id: In(userGroupIds),
+ account_id: account_id,
+ deleted_at: IsNull(),
+ },
+ comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
+ });
+ // idはユニークであるため取得件数の一致でグループの存在を確認
+ if (userGroupIds.length !== groupRecords.length) {
+ throw new TypistUserGroupNotFoundError(
+ `Group not exists Error. reqUserGroupId:${userGroupIds}; resUserGroupId:${groupRecords.map(
+ (x) => x.id,
+ )}`,
+ );
+ }
+
// 引数audioFileIdを使ってTaskレコードを特定し、そのステータスを取得/存在確認
const taskRepo = entityManager.getRepository(Task);
@@ -992,6 +1034,7 @@ export class TasksRepositoryService {
},
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
//タスクが存在しない or ステータスがUploadedでなければエラー
if (!taskRecord) {
@@ -1155,39 +1198,51 @@ export class TasksRepositoryService {
return await this.dataSource.transaction(async (entityManager) => {
// 音声ファイルを取得
const audioFileRepo = entityManager.getRepository(AudioFile);
- const audioFile = await audioFileRepo.findOne({
- relations: {
- task: true,
- },
+ const audio = await audioFileRepo.findOne({
where: {
id: audioFileId,
account_id: accountId,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
- if (!audioFile) {
+ if (!audio) {
throw new Error(
`audio file not found. audio_file_id:${audioFileId}, accountId:${accountId}`,
);
}
- const { task } = audioFile;
-
- if (!task) {
- throw new Error(
- `task not found. audio_file_id:${audioFileId}, accountId:${accountId}`,
- );
- }
// authorIdをもとにユーザーを取得
const userRepo = entityManager.getRepository(User);
const authorUser = await userRepo.findOne({
where: {
- author_id: audioFile.author_id,
+ author_id: audio.author_id,
account_id: accountId,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
+ // TaskとFileを取得
+ const taskRepo = entityManager.getRepository(Task);
+ const task = await taskRepo.findOne({
+ relations: {
+ file: true,
+ },
+ where: {
+ account_id: accountId,
+ audio_file_id: audioFileId,
+ },
+ comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
+ });
+
+ const audioFile = task?.file;
+ if (!audioFile) {
+ throw new Error(
+ `audio file not found. audio_file_id:${audioFileId}, accountId:${accountId}`,
+ );
+ }
+
// 音声ファイル上のworktypeIdをもとにworktypeを取得
const worktypeRepo = entityManager.getRepository(Worktype);
const worktypeRecord = await worktypeRepo.findOne({
@@ -1196,6 +1251,7 @@ export class TasksRepositoryService {
account_id: accountId,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// 音声ファイル上のworktypeIdが設定されているが、一致するworktypeが存在しない場合はエラーを出して終了
@@ -1217,6 +1273,7 @@ export class TasksRepositoryService {
worktype_id: worktypeRecord?.id ?? IsNull(),
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// Workflow(ルーティングルール)があればタスクのチェックアウト権限を設定する
@@ -1244,6 +1301,7 @@ export class TasksRepositoryService {
account_id: accountId,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!myAuthorUser) {
throw new Error(
@@ -1260,6 +1318,7 @@ export class TasksRepositoryService {
worktype_id: worktypeRecord?.id ?? IsNull(),
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// API実行者のAuthorIdと音声ファイルのWorktypeをもとにルーティングルールを取得できない場合はエラーを出して終了
@@ -1326,6 +1385,7 @@ export class TasksRepositoryService {
const typistUsers = await userRepo.find({
where: { account_id: accountId, id: In(typistIds) },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (typistUsers.length !== typistIds.length) {
throw new Error(`typist not found. ids: ${typistIds}`);
@@ -1339,6 +1399,7 @@ export class TasksRepositoryService {
const typistGroups = await userGroupRepo.find({
where: { account_id: accountId, id: In(groupIds) },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (typistGroups.length !== groupIds.length) {
throw new Error(`typist group not found. ids: ${groupIds}`);
diff --git a/dictation_server/src/repositories/template_files/template_files.repository.service.ts b/dictation_server/src/repositories/template_files/template_files.repository.service.ts
index e3d3319..5fc805b 100644
--- a/dictation_server/src/repositories/template_files/template_files.repository.service.ts
+++ b/dictation_server/src/repositories/template_files/template_files.repository.service.ts
@@ -52,6 +52,7 @@ export class TemplateFilesRepositoryService {
const template = await templateFilesRepo.findOne({
where: { account_id: accountId, file_name: fileName },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// 同名ファイルは同じものとして扱うため、すでにファイルがあれば更新(更新日時の履歴を残しておきたい)
diff --git a/dictation_server/src/repositories/user_groups/errors/types.ts b/dictation_server/src/repositories/user_groups/errors/types.ts
index 57aabbb..4a215a7 100644
--- a/dictation_server/src/repositories/user_groups/errors/types.ts
+++ b/dictation_server/src/repositories/user_groups/errors/types.ts
@@ -12,3 +12,10 @@ export class TypistIdInvalidError extends Error {
this.name = 'TypistIdInvalidError';
}
}
+// 同名のタイピストグループが存在する場合のエラー
+export class TypistGroupNameAlreadyExistError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'TypistGroupNameAlreadyExistError';
+ }
+}
\ No newline at end of file
diff --git a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts
index eca0bc7..a900569 100644
--- a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts
+++ b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts
@@ -1,9 +1,13 @@
import { Injectable } from '@nestjs/common';
-import { DataSource, In, IsNull } from 'typeorm';
+import { DataSource, In, IsNull, Not } from 'typeorm';
import { UserGroup } from './entity/user_group.entity';
import { UserGroupMember } from './entity/user_group_member.entity';
import { User } from '../users/entity/user.entity';
-import { TypistGroupNotExistError, TypistIdInvalidError } from './errors/types';
+import {
+ TypistGroupNameAlreadyExistError,
+ TypistGroupNotExistError,
+ TypistIdInvalidError,
+} from './errors/types';
import { USER_ROLES } from '../../constants';
import {
insertEntities,
@@ -122,6 +126,7 @@ export class UserGroupsRepositoryService {
role: USER_ROLES.TYPIST,
email_verified: true,
},
+ lock: { mode: 'pessimistic_write' },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
if (userRecords.length !== typistIds.length) {
@@ -131,6 +136,19 @@ export class UserGroupsRepositoryService {
)}`,
);
}
+ // 同名のタイピストグループが存在するか確認する
+ const sameNameTypistGroup = await userGroupRepo.findOne({
+ where: {
+ name,
+ account_id: accountId,
+ },
+ comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ });
+ if (sameNameTypistGroup) {
+ throw new TypistGroupNameAlreadyExistError(
+ `TypistGroup already exists Error. accountId: ${accountId}; name: ${name}`,
+ );
+ }
// userGroupをDBに保存する
const userGroup = await insertEntity(
UserGroup,
@@ -188,6 +206,7 @@ export class UserGroupsRepositoryService {
role: USER_ROLES.TYPIST,
email_verified: true,
},
+ lock: { mode: 'pessimistic_write' },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
if (userRecords.length !== typistIds.length) {
@@ -198,12 +217,28 @@ export class UserGroupsRepositoryService {
);
}
+ // 同名のタイピストグループが存在するか確認する
+ const sameNameTypistGroup = await userGroupRepo.findOne({
+ where: {
+ id: Not(typistGroupId),
+ name: typistGroupName,
+ account_id: accountId,
+ },
+ comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ });
+ if (sameNameTypistGroup) {
+ throw new TypistGroupNameAlreadyExistError(
+ `TypistGroup already exists Error. accountId: ${accountId}; name: ${typistGroupName}`,
+ );
+ }
+
// GroupIdが自アカウント内に存在するか確認する
const typistGroup = await userGroupRepo.findOne({
where: {
id: typistGroupId,
account_id: accountId,
},
+ lock: { mode: 'pessimistic_write' },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
if (!typistGroup) {
diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts
index ae3fccc..8c39441 100644
--- a/dictation_server/src/repositories/users/users.repository.service.ts
+++ b/dictation_server/src/repositories/users/users.repository.service.ts
@@ -289,6 +289,7 @@ export class UsersRepositoryService {
const targetUser = await repo.findOne({
where: { id: id, account_id: accountId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
diff --git a/dictation_server/src/repositories/workflows/workflows.repository.service.ts b/dictation_server/src/repositories/workflows/workflows.repository.service.ts
index cf28006..2ccaf5e 100644
--- a/dictation_server/src/repositories/workflows/workflows.repository.service.ts
+++ b/dictation_server/src/repositories/workflows/workflows.repository.service.ts
@@ -87,6 +87,7 @@ export class WorkflowsRepositoryService {
const author = await userRepo.findOne({
where: { account_id: accountId, id: authorId, email_verified: true },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!author) {
throw new UserNotFoundError(
@@ -100,6 +101,7 @@ export class WorkflowsRepositoryService {
const worktypes = await worktypeRepo.find({
where: { account_id: accountId, id: worktypeId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (worktypes.length === 0) {
throw new WorktypeIdNotFoundError(
@@ -114,6 +116,7 @@ export class WorkflowsRepositoryService {
const template = await templateRepo.findOne({
where: { account_id: accountId, id: templateId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!template) {
throw new TemplateFileNotExistError('template not found.');
@@ -131,6 +134,7 @@ export class WorkflowsRepositoryService {
email_verified: true,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (typistUsers.length !== typistIds.length) {
throw new UserNotFoundError(
@@ -146,6 +150,7 @@ export class WorkflowsRepositoryService {
const typistGroups = await userGroupRepo.find({
where: { account_id: accountId, id: In(groupIds) },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (typistGroups.length !== groupIds.length) {
throw new TypistGroupNotExistError(
@@ -163,6 +168,7 @@ export class WorkflowsRepositoryService {
worktype_id: worktypeId !== undefined ? worktypeId : IsNull(),
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (workflow.length !== 0) {
throw new AuthorIdAndWorktypeIdPairAlreadyExistsError(
@@ -227,23 +233,12 @@ export class WorkflowsRepositoryService {
): Promise {
return await this.dataSource.transaction(async (entityManager) => {
const workflowRepo = entityManager.getRepository(Workflow);
-
- // ワークフローの存在確認
- const targetWorkflow = await workflowRepo.findOne({
- where: { account_id: accountId, id: workflowId },
- comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
- });
- if (!targetWorkflow) {
- throw new WorkflowNotFoundError(
- `workflow not found. id: ${workflowId}`,
- );
- }
-
// authorの存在確認
const userRepo = entityManager.getRepository(User);
const author = await userRepo.findOne({
where: { account_id: accountId, id: authorId, email_verified: true },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!author) {
throw new UserNotFoundError(
@@ -251,12 +246,44 @@ export class WorkflowsRepositoryService {
);
}
+ // ルーティング候補ユーザーの存在確認
+ const typistIds = typists.flatMap((typist) =>
+ typist.typistId ? [typist.typistId] : [],
+ );
+ const typistUsers = await userRepo.find({
+ where: {
+ account_id: accountId,
+ id: In(typistIds),
+ email_verified: true,
+ },
+ comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
+ });
+ if (typistUsers.length !== typistIds.length) {
+ throw new UserNotFoundError(
+ `typist not found or email not verified. ids: ${typistIds}`,
+ );
+ }
+
+ // ワークフローの存在確認
+ const targetWorkflow = await workflowRepo.findOne({
+ where: { account_id: accountId, id: workflowId },
+ comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
+ });
+ if (!targetWorkflow) {
+ throw new WorkflowNotFoundError(
+ `workflow not found. id: ${workflowId}`,
+ );
+ }
+
// worktypeの存在確認
if (worktypeId !== undefined) {
const worktypeRepo = entityManager.getRepository(Worktype);
const worktypes = await worktypeRepo.find({
where: { account_id: accountId, id: worktypeId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (worktypes.length === 0) {
throw new WorktypeIdNotFoundError(
@@ -271,6 +298,7 @@ export class WorkflowsRepositoryService {
const template = await templateRepo.findOne({
where: { account_id: accountId, id: templateId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!template) {
throw new TemplateFileNotExistError(
@@ -279,24 +307,6 @@ export class WorkflowsRepositoryService {
}
}
- // ルーティング候補ユーザーの存在確認
- const typistIds = typists.flatMap((typist) =>
- typist.typistId ? [typist.typistId] : [],
- );
- const typistUsers = await userRepo.find({
- where: {
- account_id: accountId,
- id: In(typistIds),
- email_verified: true,
- },
- comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
- });
- if (typistUsers.length !== typistIds.length) {
- throw new UserNotFoundError(
- `typist not found or email not verified. ids: ${typistIds}`,
- );
- }
-
// ルーティング候補ユーザーグループの存在確認
const groupIds = typists.flatMap((typist) => {
return typist.typistGroupId ? [typist.typistGroupId] : [];
@@ -305,6 +315,7 @@ export class WorkflowsRepositoryService {
const typistGroups = await userGroupRepo.find({
where: { account_id: accountId, id: In(groupIds) },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (typistGroups.length !== groupIds.length) {
throw new TypistGroupNotExistError(
@@ -399,6 +410,7 @@ export class WorkflowsRepositoryService {
const workflow = await workflowRepo.findOne({
where: { account_id: accountId, id: workflowId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (!workflow) {
throw new WorkflowNotFoundError(
diff --git a/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts b/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts
index f647288..8ca02c9 100644
--- a/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts
+++ b/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts
@@ -88,6 +88,7 @@ export class WorktypesRepositoryService {
const duplicatedWorktype = await worktypeRepo.findOne({
where: { account_id: accountId, custom_worktype_id: worktypeId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// ワークタイプIDが重複している場合はエラー
@@ -100,6 +101,7 @@ export class WorktypesRepositoryService {
const worktypeCount = await worktypeRepo.count({
where: { account_id: accountId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// ワークタイプの登録数が上限に達している場合はエラー
@@ -163,6 +165,7 @@ export class WorktypesRepositoryService {
const worktype = await worktypeRepo.findOne({
where: { account_id: accountId, id: id },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// ワークタイプが存在しない場合はエラー
@@ -177,6 +180,7 @@ export class WorktypesRepositoryService {
id: Not(id),
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// ワークタイプIDが重複している場合はエラー
@@ -216,6 +220,7 @@ export class WorktypesRepositoryService {
const worktype = await worktypeRepo.findOne({
where: { account_id: accountId, id: id },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// ワークタイプが存在しない場合はエラー
if (!worktype) {
@@ -227,6 +232,7 @@ export class WorktypesRepositoryService {
const account = await accountRepo.findOne({
where: { id: accountId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (account?.active_worktype_id === id) {
@@ -244,6 +250,7 @@ export class WorktypesRepositoryService {
const workflows = await workflowRepo.find({
where: { account_id: accountId, worktype_id: id },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
if (workflows.length > 0) {
const workflowIds = workflows.map((workflow) => workflow.id);
@@ -322,6 +329,7 @@ export class WorktypesRepositoryService {
const worktype = await worktypeRepo.findOne({
where: { account_id: accountId, id: worktypeId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
+ lock: { mode: 'pessimistic_write' },
});
// ワークタイプが存在しない場合はエラー
if (!worktype) {
|