Merge branch 'develop'

This commit is contained in:
SAITO-PC-3\saito.k 2024-02-06 16:26:58 +09:00
commit f978d837e7
51 changed files with 1211 additions and 792 deletions

View File

@ -0,0 +1,4 @@
-- [OMDS_IS-231] アカウントIDの開始番号調整 | 課題の表示 | Backlog 対応
-- IDからアカウント数が推測されるため、ユーザ指定の任意値を最初の番号とする
-- 一度しか実行しないため、migrate fileではなくDBの初期値として扱う。移行時の実行を想定
ALTER TABLE accounts AUTO_INCREMENT = 853211;

View File

@ -54,6 +54,8 @@ export const errorCodes = [
"E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合) "E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
"E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合) "E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
"E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) "E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
"E010908", // タイピストグループ不在エラー
"E010909", // タイピストグループ名重複エラー
"E011001", // ワークタイプ重複エラー "E011001", // ワークタイプ重複エラー
"E011002", // ワークタイプ登録上限超過エラー "E011002", // ワークタイプ登録上限超過エラー
"E011003", // ワークタイプ不在エラー "E011003", // ワークタイプ不在エラー

View File

@ -43,7 +43,12 @@ export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [
* *
* @const {string[]} * @const {string[]}
*/ */
export const KEYS_TO_PRESERVE = ["accessToken", "refreshToken", "displayInfo"]; export const KEYS_TO_PRESERVE = [
"accessToken",
"refreshToken",
"displayInfo",
"sortCriteria",
];
/** /**
* *

View File

@ -28,6 +28,13 @@ export const SORTABLE_COLUMN = {
export type SortableColumnType = export type SortableColumnType =
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; 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 = export type SortableColumnList =
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN];
@ -38,6 +45,10 @@ export const DIRECTION = {
export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION]; export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION];
// DirectionTypeの型チェック関数
export const isDirectionType = (arg: string): arg is DirectionType =>
arg in DIRECTION;
export interface DisplayInfoType { export interface DisplayInfoType {
JobNumber: boolean; JobNumber: boolean;
Status: boolean; Status: boolean;

View File

@ -280,7 +280,6 @@ export const playbackAsync = createAsyncThunk<
direction: DirectionType; direction: DirectionType;
paramName: SortableColumnType; paramName: SortableColumnType;
audioFileId: number; audioFileId: number;
isTypist: boolean;
}, },
{ {
// rejectした時の返却値の型 // rejectした時の返却値の型
@ -289,7 +288,7 @@ export const playbackAsync = createAsyncThunk<
}; };
} }
>("dictations/playbackAsync", async (args, thunkApi) => { >("dictations/playbackAsync", async (args, thunkApi) => {
const { audioFileId, direction, paramName, isTypist } = args; const { audioFileId, direction, paramName } = args;
// apiのConfigurationを取得する // apiのConfigurationを取得する
const { getState } = thunkApi; const { getState } = thunkApi;
@ -300,15 +299,12 @@ export const playbackAsync = createAsyncThunk<
const tasksApi = new TasksApi(config); const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config); const usersApi = new UsersApi(config);
try { try {
// ユーザーがタイピストである場合に、ソート条件を保存する
if (isTypist) {
await usersApi.updateSortCriteria( await usersApi.updateSortCriteria(
{ direction, paramName }, { direction, paramName },
{ {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
} }
); );
}
await tasksApi.checkout(audioFileId, { await tasksApi.checkout(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` }, headers: { authorization: `Bearer ${accessToken}` },
}); });

View File

@ -62,6 +62,12 @@ export const orderLicenseAsync = createAsyncThunk<
); );
} }
if (error.code === "E010501") {
errorMessage = getTranslationID(
"licenseOrderPage.message.dealerNotFoundError"
);
}
thunkApi.dispatch( thunkApi.dispatch(
openSnackbar({ openSnackbar({
level: "error", level: "error",

View File

@ -122,11 +122,17 @@ export const createTypistGroupAsync = createAsyncThunk<
} catch (e) { } catch (e) {
// e ⇒ errorObjectに変換" // e ⇒ errorObjectに変換"
const error = createErrorObject(e); const error = createErrorObject(e);
let message = getTranslationID("common.message.internalServerError");
const message = if (error.code === "E010204") {
error.statusCode === 400 message = getTranslationID(
? getTranslationID("typistGroupSetting.message.groupSaveFailedError") "typistGroupSetting.message.groupSaveFailedError"
: getTranslationID("common.message.internalServerError"); );
}
if (error.code === "E010909") {
message = getTranslationID(
"typistGroupSetting.message.GroupNameAlreadyExistError"
);
}
thunkApi.dispatch( thunkApi.dispatch(
openSnackbar({ openSnackbar({
@ -242,10 +248,17 @@ export const updateTypistGroupAsync = createAsyncThunk<
// e ⇒ errorObjectに変換" // e ⇒ errorObjectに変換"
const error = createErrorObject(e); const error = createErrorObject(e);
const message = let message = getTranslationID("common.message.internalServerError");
error.statusCode === 400 if (error.code === "E010204" || error.code === "E010908") {
? getTranslationID("typistGroupSetting.message.groupSaveFailedError") message = getTranslationID(
: getTranslationID("common.message.internalServerError"); "typistGroupSetting.message.groupSaveFailedError"
);
}
if (error.code === "E010909") {
message = getTranslationID(
"typistGroupSetting.message.GroupNameAlreadyExistError"
);
}
thunkApi.dispatch( thunkApi.dispatch(
openSnackbar({ openSnackbar({

View File

@ -191,6 +191,7 @@ const AccountPage: React.FC = (): JSX.Element => {
)} )}
</dt> </dt>
{isTier5 && ( {isTier5 && (
<>
<dd> <dd>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label> <label>
@ -208,6 +209,14 @@ const AccountPage: React.FC = (): JSX.Element => {
/> />
</label> </label>
</dd> </dd>
<dd className={`${styles.full} ${styles.formComment}`}>
{t(
getTranslationID(
"accountPage.text.dealerManagementAnnotation"
)
)}
</dd>
</>
)} )}
{!isTier5 && <dd>-</dd>} {!isTier5 && <dd>-</dd>}
</dl> </dl>
@ -374,6 +383,15 @@ const AccountPage: React.FC = (): JSX.Element => {
className={styles.icLoading} className={styles.icLoading}
alt="Loading" alt="Loading"
/> />
{isTier5 && (
<p className={styles.formComment}>
{t(
getTranslationID(
"accountPage.text.dealerManagementAnnotation"
)
)}
</p>
)}
</div> </div>
</div> </div>
{isTier5 && ( {isTier5 && (

View File

@ -33,6 +33,8 @@ import {
playbackAsync, playbackAsync,
cancelAsync, cancelAsync,
PRIORITY, PRIORITY,
isSortableColumnType,
isDirectionType,
} from "features/dictation"; } from "features/dictation";
import { getTranslationID } from "translation"; import { getTranslationID } from "translation";
import { Task } from "api/api"; import { Task } from "api/api";
@ -242,6 +244,12 @@ const DictationPage: React.FC = (): JSX.Element => {
dispatch(changeDirection({ direction: currentDirection })); dispatch(changeDirection({ direction: currentDirection }));
dispatch(changeParamName({ paramName })); dispatch(changeParamName({ paramName }));
// ローカルストレージにソート情報を保存する
localStorage.setItem(
"sortCriteria",
`direction:${currentDirection},paramName:${paramName}`
);
const filter = getFilter( const filter = getFilter(
filterUploaded, filterUploaded,
filterInProgress, filterInProgress,
@ -348,10 +356,11 @@ const DictationPage: React.FC = (): JSX.Element => {
audioFileId, audioFileId,
direction: sortDirection, direction: sortDirection,
paramName: sortableParamName, paramName: sortableParamName,
isTypist,
}) })
); );
if (meta.requestStatus === "fulfilled") { if (meta.requestStatus === "fulfilled") {
// ローカルストレージにソート情報を削除する
localStorage.removeItem("sortCriteria");
const filter = getFilter( const filter = getFilter(
filterUploaded, filterUploaded,
filterInProgress, filterInProgress,
@ -388,7 +397,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filterInProgress, filterInProgress,
filterPending, filterPending,
filterUploaded, filterUploaded,
isTypist,
sortDirection, sortDirection,
sortableParamName, sortableParamName,
t, t,
@ -522,13 +530,39 @@ const DictationPage: React.FC = (): JSX.Element => {
dispatch(changeDisplayInfo({ column: displayInfo })); dispatch(changeDisplayInfo({ column: displayInfo }));
const filter = getFilter(true, true, true, true, false); const filter = getFilter(true, true, true, true, false);
const { meta, payload } = await dispatch(getSortColumnAsync()); const { meta, payload } = await dispatch(getSortColumnAsync());
if ( if (
meta.requestStatus === "fulfilled" && meta.requestStatus === "fulfilled" &&
payload && payload &&
!("error" in 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( dispatch(
listTasksAsync({ listTasksAsync({
limit: LIMIT_TASK_NUM, limit: LIMIT_TASK_NUM,
@ -1082,7 +1116,13 @@ const DictationPage: React.FC = (): JSX.Element => {
{(isChangeTranscriptionistPopupOpen || !isLoading) && {(isChangeTranscriptionistPopupOpen || !isLoading) &&
tasks.length !== 0 && tasks.length !== 0 &&
tasks.map((x) => ( tasks.map((x) => (
<tr key={x.audioFileId}> <tr
key={x.audioFileId}
style={{
backgroundColor:
x.priority === "01" ? "#ff00004f" : "#ffffff",
}}
>
<td className={styles.clm0}> <td className={styles.clm0}>
<ul className={styles.menuInTable}> <ul className={styles.menuInTable}>
<li> <li>

View File

@ -85,7 +85,7 @@ export const AddWorktypeIdPopup: React.FC<AddWorktypeIdPopupProps> = (
<input <input
type="text" type="text"
size={40} size={40}
maxLength={255} maxLength={16}
value={worktypeId ?? ""} value={worktypeId ?? ""}
className={styles.formInput} className={styles.formInput}
onChange={(e) => { onChange={(e) => {

View File

@ -84,7 +84,7 @@ export const EditWorktypeIdPopup: React.FC<EditWorktypeIdPopupProps> = (
<input <input
type="text" type="text"
size={40} size={40}
maxLength={255} maxLength={16}
value={worktypeId ?? ""} value={worktypeId ?? ""}
className={styles.formInput} className={styles.formInput}
onChange={(e) => { onChange={(e) => {

View File

@ -1343,23 +1343,23 @@ _:-ms-lang(x)::-ms-backdrop,
.tableHeader th .hasSort:hover::before { .tableHeader th .hasSort:hover::before {
opacity: 1; 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 { .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; opacity: 1;
} }
.tableHeader th .hasSort.isActiveZa:hover::before { .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-top: 0.4rem #ffffff solid;
border-right: 0.35rem transparent solid; border-right: 0.35rem transparent solid;
border-bottom: none; border-bottom: none;
@ -1632,8 +1632,31 @@ _:-ms-lang(x)::-ms-backdrop,
.account .listVertical dd .formInput { .account .listVertical dd .formInput {
max-width: 100%; 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 { .account .box100.alignRight {
width: calc(1200px + 3rem); width: calc(1200px + 3rem);
text-align: right;
}
.account .box100.alignRight .formComment {
margin-left: 648px;
text-align: right;
} }
.menuAction { .menuAction {
@ -2306,7 +2329,8 @@ tr.isSelected .menuInTable li a.isDisable {
} }
.formChange ul.chooseMember li input + label:hover, .formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember 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; background-size: 1.3rem;
} }
.formChange ul.chooseMember li input:checked + label, .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.chooseMember li input:checked + label:hover,
.formChange ul.holdMember 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 background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
center; right center;
background-size: 1.3rem; background-size: 1.3rem;
} }
.formChange > p { .formChange > p {
@ -2471,7 +2495,8 @@ tr.isSelected .menuInTable li a.isDisable {
} }
.formChange ul.chooseMember li input + label:hover, .formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember 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; background-size: 1.3rem;
} }
.formChange ul.chooseMember li input:checked + label, .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.chooseMember li input:checked + label:hover,
.formChange ul.holdMember 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 background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
center; right center;
background-size: 1.3rem; background-size: 1.3rem;
} }
.formChange > p { .formChange > p {

View File

@ -89,8 +89,8 @@ declare const classNames: {
readonly snackbarIcon: "snackbarIcon"; readonly snackbarIcon: "snackbarIcon";
readonly snackbarIconClose: "snackbarIconClose"; readonly snackbarIconClose: "snackbarIconClose";
readonly hasSort: "hasSort"; readonly hasSort: "hasSort";
readonly isActiveAz: "isActiveAz";
readonly isActiveZa: "isActiveZa"; readonly isActiveZa: "isActiveZa";
readonly isActiveAz: "isActiveAz";
readonly noLine: "noLine"; readonly noLine: "noLine";
readonly home: "home"; readonly home: "home";
readonly pgHome: "pgHome"; readonly pgHome: "pgHome";
@ -107,6 +107,7 @@ declare const classNames: {
readonly clm0: "clm0"; readonly clm0: "clm0";
readonly menuInTable: "menuInTable"; readonly menuInTable: "menuInTable";
readonly isSelected: "isSelected"; readonly isSelected: "isSelected";
readonly odd: "odd";
readonly alignRight: "alignRight"; readonly alignRight: "alignRight";
readonly menuAction: "menuAction"; readonly menuAction: "menuAction";
readonly inTable: "inTable"; readonly inTable: "inTable";

View File

@ -24,7 +24,7 @@
"headerDictations": "Diktate", "headerDictations": "Diktate",
"headerWorkflow": "Arbeitsablauf", "headerWorkflow": "Arbeitsablauf",
"headerPartners": "Partner", "headerPartners": "Partner",
"headerSupport": "(de)Support", "headerSupport": "Support",
"tier1": "Admin", "tier1": "Admin",
"tier2": "BC", "tier2": "BC",
"tier3": "Verteiler", "tier3": "Verteiler",
@ -76,6 +76,7 @@
"linkOfEula": "Klicken Sie hier, um die Endbenutzer-Lizenzvereinbarung zu lesen.", "linkOfEula": "Klicken Sie hier, um die Endbenutzer-Lizenzvereinbarung zu lesen.",
"linkOfPrivacyNotice": "Klicken Sie hier, um die Datenschutzerklärung zu lesen.", "linkOfPrivacyNotice": "Klicken Sie hier, um die Datenschutzerklärung zu lesen.",
"forOdms": "für ODMS Cloud.", "forOdms": "für ODMS Cloud.",
"termsCheckBox": "Ja, ich stimme den Nutzungsbedingungen zu.",
"createAccountButton": "Einreichen" "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.", "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.", "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.", "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": { "label": {
"title": "Benutzer", "title": "Benutzer",
@ -179,8 +187,8 @@
"storageSize": "Lagerung verfügbar", "storageSize": "Lagerung verfügbar",
"usedSize": "Gebrauchter Lagerung", "usedSize": "Gebrauchter Lagerung",
"storageAvailable": "Speicher nicht verfügbar (Menge überschritten)", "storageAvailable": "Speicher nicht verfügbar (Menge überschritten)",
"licenseLabel": "(de)License", "licenseLabel": "Lizenz",
"storageLabel": "(de)Storage" "storageLabel": "Lagerung"
} }
}, },
"licenseOrderPage": { "licenseOrderPage": {
@ -189,7 +197,8 @@
"poNumberIncorrectError": "Das Format der Bestellnummer ist ungültig. Für die Bestellnummer können nur alphanumerische Zeichen eingegeben werden.", "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.", "newOrderIncorrectError": "Bitte geben Sie für die neue Bestellung eine Zahl größer oder gleich 1 ein.",
"confirmOrder": "Möchten Sie eine Bestellung aufgeben?", "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": { "label": {
"title": "Lizenz bestellen", "title": "Lizenz bestellen",
@ -205,8 +214,9 @@
"noPlaybackAuthorization": "Sie haben keine Berechtigung zum Abspielen dieser Datei.", "noPlaybackAuthorization": "Sie haben keine Berechtigung zum Abspielen dieser Datei.",
"taskToPlaybackNoExists": "Die Datei kann nicht abgespielt werden, da sie bereits transkribiert wurde oder nicht existiert.", "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.", "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)ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", "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": "(de)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。" "cancelFailedError": "Die Diktate konnten nicht gelöscht werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.",
"deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。"
}, },
"label": { "label": {
"title": "Diktate", "title": "Diktate",
@ -249,9 +259,9 @@
"deleteDictation": "Diktat löschen", "deleteDictation": "Diktat löschen",
"selectedTranscriptionist": "Ausgewählter transkriptionist", "selectedTranscriptionist": "Ausgewählter transkriptionist",
"poolTranscriptionist": "Transkriptionsliste", "poolTranscriptionist": "Transkriptionsliste",
"fileBackup": "(de)File Backup", "fileBackup": "Dateisicherung",
"downloadForBackup": "(de)Download for backup", "downloadForBackup": "Zur Sicherung herunterladen",
"applications": "(de)Applications", "applications": "Desktopanwendung",
"cancelDictation": "Transkription abbrechen" "cancelDictation": "Transkription abbrechen"
} }
}, },
@ -414,7 +424,10 @@
}, },
"message": { "message": {
"selectedTypistEmptyError": "Um eine Transkriptionsgruppe zu speichern, müssen ein oder mehrere Transkriptionisten ausgewählt werden.", "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": { "worktypeIdSetting": {
@ -470,7 +483,7 @@
"addAccount": "Konto hinzufügen", "addAccount": "Konto hinzufügen",
"name": "Name der Firma", "name": "Name der Firma",
"category": "Kontoebene", "category": "Kontoebene",
"accountId": "Konto-ID", "accountId": "Autoren-ID",
"country": "Land", "country": "Land",
"primaryAdmin": "Hauptadministrator", "primaryAdmin": "Hauptadministrator",
"email": "Email", "email": "Email",
@ -506,6 +519,9 @@
}, },
"message": { "message": {
"updateAccountFailedError": "Kontoinformationen konnten nicht gespeichert werden. Bitte aktualisieren Sie den Bildschirm und versuchen Sie es erneut." "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": { "deleteAccountPopup": {
@ -537,22 +553,22 @@
}, },
"supportPage": { "supportPage": {
"label": { "label": {
"title": "(de)Support", "title": "Support",
"howToUse": "(de)How to use the system", "howToUse": "So verwenden Sie das System",
"supportPageEnglish": "OMDS Cloud User Guide", "supportPageEnglish": "OMDS Cloud User Guide",
"supportPageGerman": "OMDS Cloud Benutzerhandbuch", "supportPageGerman": "OMDS Cloud-Benutzerhandbuch",
"supportPageFrench": "OMDS Cloud Mode d'emploi", "supportPageFrench": "Guía del usuario de la nube OMDS",
"supportPageSpanish": "OMDS Cloud Guía del usuario" "supportPageSpanish": "Guide de l'utilisateur du cloud OMDS"
}, },
"text": { "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": { "filePropertyPopup": {
"label": { "label": {
"general": "(de)General", "general": "Allgemein",
"job": "(de)Job", "job": "Aufgabe",
"close": "(de)Close" "close": "Schließen"
} }
} }
} }

View File

@ -75,7 +75,7 @@
"password": "Password", "password": "Password",
"linkOfEula": "Click here to read the End User License Agreement.", "linkOfEula": "Click here to read the End User License Agreement.",
"linkOfPrivacyNotice": "Click here to read the Privacy Notice.", "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.", "termsCheckBox": "Yes, I agree to the terms of use.",
"createAccountButton": "Submit" "createAccountButton": "Submit"
} }
@ -128,7 +128,14 @@
"authorIdIncorrectError": "Author ID format is invalid. Only alphanumeric characters and \"_\" can be entered for Author ID.", "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.", "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.", "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": { "label": {
"title": "User", "title": "User",
@ -190,7 +197,8 @@
"poNumberIncorrectError": "PO Number format is not valid. Only alphanumeric characters can be entered for the PO Number.", "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.", "newOrderIncorrectError": "Please enter a number greater than or equal to 1 for the New Order.",
"confirmOrder": "Would you like to place an 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": { "label": {
"title": "Order License", "title": "Order License",
@ -206,8 +214,9 @@
"noPlaybackAuthorization": "You do not have permission to playback this file.", "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.", "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.", "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": "ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", "backupFailedError": "The \"File Backup\" process has failed. Please try again later. If the error continues, contact your system administrator.",
"cancelFailedError": "タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。" "cancelFailedError": "Failed to delete the dictations. Please refresh your screen and try again.",
"deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。"
}, },
"label": { "label": {
"title": "Dictations", "title": "Dictations",
@ -252,7 +261,7 @@
"poolTranscriptionist": "Transcription List", "poolTranscriptionist": "Transcription List",
"fileBackup": "File Backup", "fileBackup": "File Backup",
"downloadForBackup": "Download for backup", "downloadForBackup": "Download for backup",
"applications": "Applications", "applications": "Desktop Application",
"cancelDictation": "Cancel Transcription" "cancelDictation": "Cancel Transcription"
} }
}, },
@ -415,7 +424,10 @@
}, },
"message": { "message": {
"selectedTypistEmptyError": "One or more transcriptonist must be selected to save a transcrption group.", "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": { "worktypeIdSetting": {
@ -507,6 +519,9 @@
}, },
"message": { "message": {
"updateAccountFailedError": "Failed to save account information. Please refresh the screen and try again." "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": { "deleteAccountPopup": {
@ -541,12 +556,12 @@
"title": "Support", "title": "Support",
"howToUse": "How to use the system", "howToUse": "How to use the system",
"supportPageEnglish": "OMDS Cloud User Guide", "supportPageEnglish": "OMDS Cloud User Guide",
"supportPageGerman": "OMDS Cloud Benutzerhandbuch", "supportPageGerman": "OMDS Cloud-Benutzerhandbuch",
"supportPageFrench": "OMDS Cloud Mode d'emploi", "supportPageFrench": "Guía del usuario de la nube OMDS",
"supportPageSpanish": "OMDS Cloud Guía del usuario" "supportPageSpanish": "Guide de l'utilisateur du cloud OMDS"
}, },
"text": { "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": { "filePropertyPopup": {

View File

@ -24,11 +24,11 @@
"headerDictations": "Dictado", "headerDictations": "Dictado",
"headerWorkflow": "flujo de trabajo", "headerWorkflow": "flujo de trabajo",
"headerPartners": "Socios", "headerPartners": "Socios",
"headerSupport": "(es)Support", "headerSupport": "Soporte",
"tier1": "Admin", "tier1": "Admin",
"tier2": "BC", "tier2": "BC",
"tier3": "Distribuidor", "tier3": "Distribuidor",
"tier4": "Concesionario", "tier4": "Distribuidor",
"tier5": "Cliente", "tier5": "Cliente",
"notSelected": "Ninguno", "notSelected": "Ninguno",
"signOutButton": "cerrar sesión" "signOutButton": "cerrar sesión"
@ -62,14 +62,14 @@
"title": "Crea tu cuenta", "title": "Crea tu cuenta",
"accountInfoTitle": "Información de Registro", "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.", "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", "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)." "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": { "label": {
"company": "Nombre de empresa", "company": "Nombre de empresa",
"country": "País", "country": "País",
"dealer": "Concesionario (Opcional)", "dealer": "Distribuidor (Opcional)",
"adminName": "Nombre del administrador", "adminName": "Nombre del administrador",
"email": "Dirección de correo electrónico", "email": "Dirección de correo electrónico",
"password": "Contraseña", "password": "Contraseña",
@ -93,7 +93,7 @@
"label": { "label": {
"company": "Nombre de empresa", "company": "Nombre de empresa",
"country": "País", "country": "País",
"dealer": "Concesionario (Opcional)", "dealer": "Distribuidor (Opcional)",
"adminName": "Nombre del administrador", "adminName": "Nombre del administrador",
"email": "Dirección de correo electrónico", "email": "Dirección de correo electrónico",
"password": "Contraseña", "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.", "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.", "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.", "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": { "label": {
"title": "Usuario", "title": "Usuario",
@ -180,8 +187,8 @@
"storageSize": "Almacenamiento disponible", "storageSize": "Almacenamiento disponible",
"usedSize": "Almacenamiento utilizado", "usedSize": "Almacenamiento utilizado",
"storageAvailable": "Almacenamiento no disponible (cantidad excedida)", "storageAvailable": "Almacenamiento no disponible (cantidad excedida)",
"licenseLabel": "(es)License", "licenseLabel": "Licencia",
"storageLabel": "(es)Storage" "storageLabel": "Almacenamiento"
} }
}, },
"licenseOrderPage": { "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.", "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.", "newOrderIncorrectError": "Ingrese un número mayor o igual a 1 para el Nuevo Pedido.",
"confirmOrder": "¿Quieres hacer un 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": { "label": {
"title": "Licencia de pedido", "title": "Licencia de pedido",
@ -206,8 +214,9 @@
"noPlaybackAuthorization": "No tienes permiso para reproducir este archivo.", "noPlaybackAuthorization": "No tienes permiso para reproducir este archivo.",
"taskToPlaybackNoExists": "El archivo no se puede reproducir porque ya ha sido transcrito o no existe.", "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.", "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)ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", "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": "(es)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。" "cancelFailedError": "No se pudieron eliminar los dictados. Actualice su pantalla e inténtelo nuevamente.",
"deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。"
}, },
"label": { "label": {
"title": "Dictado", "title": "Dictado",
@ -250,9 +259,9 @@
"deleteDictation": "Borrar dictado", "deleteDictation": "Borrar dictado",
"selectedTranscriptionist": "Transcriptor seleccionado", "selectedTranscriptionist": "Transcriptor seleccionado",
"poolTranscriptionist": "Lista de transcriptor", "poolTranscriptionist": "Lista de transcriptor",
"fileBackup": "(es)File Backup", "fileBackup": "Copia de seguridad de archivos",
"downloadForBackup": "(es)Download for backup", "downloadForBackup": "Descargar para respaldo",
"applications": "(es)Applications", "applications": "Aplicación de escritorio",
"cancelDictation": "Cancelar transcripción" "cancelDictation": "Cancelar transcripción"
} }
}, },
@ -415,7 +424,10 @@
}, },
"message": { "message": {
"selectedTypistEmptyError": "Se deben seleccionar uno o más transcriptores para guardar un grupo de transcripción.", "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": { "worktypeIdSetting": {
@ -471,11 +483,11 @@
"addAccount": "Añadir cuenta", "addAccount": "Añadir cuenta",
"name": "Nombre de empresa", "name": "Nombre de empresa",
"category": "Nivel de cuenta", "category": "Nivel de cuenta",
"accountId": "ID de la cuenta", "accountId": "ID de autor",
"country": "País", "country": "País",
"primaryAdmin": "Administrador primario", "primaryAdmin": "Administrador primario",
"email": "Email", "email": "Email",
"dealerManagement": "Permitir que el concesionario realice los cambios", "dealerManagement": "Permitir que el distribuidor realice los cambios",
"partners": "Socios", "partners": "Socios",
"deleteAccount": "Borrar cuenta" "deleteAccount": "Borrar cuenta"
}, },
@ -494,9 +506,9 @@
"accountID": "ID de la cuenta", "accountID": "ID de la cuenta",
"yourCategory": "Tipo de cuenta", "yourCategory": "Tipo de cuenta",
"yourCountry": "País", "yourCountry": "País",
"yourDealer": "Concesionario", "yourDealer": "Distribuidor",
"selectDealer": "Seleccionar Concesionario", "selectDealer": "Seleccionar distribuidor",
"dealerManagement": "Permitir que el concesionario realice los cambios", "dealerManagement": "Permitir que el distribuidor realice los cambios",
"administratorInformation": "Información del administrador", "administratorInformation": "Información del administrador",
"primaryAdministrator": "Administrador primario", "primaryAdministrator": "Administrador primario",
"secondaryAdministrator": "Administrador secundario", "secondaryAdministrator": "Administrador secundario",
@ -507,6 +519,9 @@
}, },
"message": { "message": {
"updateAccountFailedError": "No se pudo guardar la información de la cuenta. Actualice la pantalla e inténtelo de nuevo." "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": { "deleteAccountPopup": {
@ -538,22 +553,22 @@
}, },
"supportPage": { "supportPage": {
"label": { "label": {
"title": "(es)Support", "title": "Soporte",
"howToUse": "(es)How to use the system", "howToUse": "Cómo utilizar el sistema",
"supportPageEnglish": "OMDS Cloud User Guide", "supportPageEnglish": "OMDS Cloud User Guide",
"supportPageGerman": "OMDS Cloud Benutzerhandbuch", "supportPageGerman": "OMDS Cloud-Benutzerhandbuch",
"supportPageFrench": "OMDS Cloud Mode d'emploi", "supportPageFrench": "Guía del usuario de la nube OMDS",
"supportPageSpanish": "OMDS Cloud Guía del usuario" "supportPageSpanish": "Guide de l'utilisateur du cloud OMDS"
}, },
"text": { "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": { "filePropertyPopup": {
"label": { "label": {
"general": "(es)General", "general": "General",
"job": "(es)Job", "job": "Trabajo",
"close": "(es)Close" "close": "Cerrar"
} }
} }
} }

View File

@ -24,11 +24,11 @@
"headerDictations": "Dictées", "headerDictations": "Dictées",
"headerWorkflow": "Flux de travail", "headerWorkflow": "Flux de travail",
"headerPartners": "Partenaires", "headerPartners": "Partenaires",
"headerSupport": "(fr)Support", "headerSupport": "Support",
"tier1": "Admin", "tier1": "Admin",
"tier2": "BC", "tier2": "BC",
"tier3": "Distributeur", "tier3": "Distributeur",
"tier4": "Concessionnaire", "tier4": "Revendeur",
"tier5": "Client", "tier5": "Client",
"notSelected": "Aucune", "notSelected": "Aucune",
"signOutButton": "se déconnecter" "signOutButton": "se déconnecter"
@ -62,14 +62,14 @@
"title": "Créez votre compte", "title": "Créez votre compte",
"accountInfoTitle": "Information d'inscription", "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.", "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", "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)." "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": { "label": {
"company": "Nom de l'entreprise", "company": "Nom de l'entreprise",
"country": "Pays", "country": "Pays",
"dealer": "Concessionnaire (Facultatif)", "dealer": "Revendeur (Facultatif)",
"adminName": "Nom de l'administrateur", "adminName": "Nom de l'administrateur",
"email": "Adresse e-mail", "email": "Adresse e-mail",
"password": "Mot de passe", "password": "Mot de passe",
@ -93,7 +93,7 @@
"label": { "label": {
"company": "Nom de l'entreprise", "company": "Nom de l'entreprise",
"country": "Pays", "country": "Pays",
"dealer": "Concessionnaire (Facultatif)", "dealer": "Revendeur (Facultatif)",
"adminName": "Nom de l'administrateur", "adminName": "Nom de l'administrateur",
"email": "Adresse e-mail", "email": "Adresse e-mail",
"password": "Mot de passe", "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.", "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.", "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.", "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": { "label": {
"title": "Utilisateur", "title": "Utilisateur",
@ -180,8 +187,8 @@
"storageSize": "Stockage disponible", "storageSize": "Stockage disponible",
"usedSize": "Stockage utilisé", "usedSize": "Stockage utilisé",
"storageAvailable": "Stockage indisponible (montant dépassée)", "storageAvailable": "Stockage indisponible (montant dépassée)",
"licenseLabel": "(fr)License", "licenseLabel": "Licence",
"storageLabel": "(fr)Storage" "storageLabel": "Stockage"
} }
}, },
"licenseOrderPage": { "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.", "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.", "newOrderIncorrectError": "Veuillez saisir un nombre supérieur ou égal à 1 pour la nouvelle commande.",
"confirmOrder": "Voulez-vous passer 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": { "label": {
"title": "Commander licence", "title": "Commander licence",
@ -206,8 +214,9 @@
"noPlaybackAuthorization": "Vous n'êtes pas autorisé à lire ce fichier.", "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.", "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.", "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)ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", "backupFailedError": "Le processus de « Sauvegarde de fichier » a échoué. Veuillez réessayer plus tard. Si l'erreur persiste, contactez votre administrateur système.",
"cancelFailedError": "(fr)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。" "cancelFailedError": "Échec de la suppression des dictées. Veuillez actualiser votre écran et réessayer.",
"deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。"
}, },
"label": { "label": {
"title": "Dictées", "title": "Dictées",
@ -250,9 +259,9 @@
"deleteDictation": "Supprimer la dictée", "deleteDictation": "Supprimer la dictée",
"selectedTranscriptionist": "Transcriptionniste sélectionné", "selectedTranscriptionist": "Transcriptionniste sélectionné",
"poolTranscriptionist": "Liste de transcriptionniste", "poolTranscriptionist": "Liste de transcriptionniste",
"fileBackup": "(fr)File Backup", "fileBackup": "Sauvegarde de fichiers",
"downloadForBackup": "(fr)Download for backup", "downloadForBackup": "Télécharger pour sauvegarde",
"applications": "(fr)Applications", "applications": "Application de bureau",
"cancelDictation": "Annuler la transcription" "cancelDictation": "Annuler la transcription"
} }
}, },
@ -415,7 +424,10 @@
}, },
"message": { "message": {
"selectedTypistEmptyError": "Un ou plusieurs transcripteurs doivent être sélectionnés pour enregistrer un groupe de transcription.", "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": { "worktypeIdSetting": {
@ -471,11 +483,11 @@
"addAccount": "Ajouter compte", "addAccount": "Ajouter compte",
"name": "Nom de l'entreprise", "name": "Nom de l'entreprise",
"category": "Niveau compte", "category": "Niveau compte",
"accountId": "identifiant de compte", "accountId": "Identifiant Auteur",
"country": "Pays", "country": "Pays",
"primaryAdmin": "Administrateur principal", "primaryAdmin": "Administrateur principal",
"email": "Email", "email": "Email",
"dealerManagement": "Autoriser le concessionnaire à modifier les paramètres", "dealerManagement": "Autoriser le revendeur à modifier les paramètres",
"partners": "Partenaires", "partners": "Partenaires",
"deleteAccount": "Supprimer le compte" "deleteAccount": "Supprimer le compte"
}, },
@ -494,9 +506,9 @@
"accountID": "identifiant de compte", "accountID": "identifiant de compte",
"yourCategory": "Type de compte", "yourCategory": "Type de compte",
"yourCountry": "Pays", "yourCountry": "Pays",
"yourDealer": "Concessionnaire", "yourDealer": "Revendeur",
"selectDealer": "Sélectionner le Concessionnaire", "selectDealer": "Sélectionner le revendeur",
"dealerManagement": "Autoriser le concessionnaire à modifier les paramètres", "dealerManagement": "Autoriser le revendeur à modifier les paramètres",
"administratorInformation": "Informations sur l'administrateur", "administratorInformation": "Informations sur l'administrateur",
"primaryAdministrator": "Administrateur principal", "primaryAdministrator": "Administrateur principal",
"secondaryAdministrator": "Administrateur secondaire", "secondaryAdministrator": "Administrateur secondaire",
@ -507,6 +519,9 @@
}, },
"message": { "message": {
"updateAccountFailedError": "Échec de l'enregistrement des informations du compte. Veuillez actualiser l'écran et réessayer." "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": { "deleteAccountPopup": {
@ -538,22 +553,22 @@
}, },
"supportPage": { "supportPage": {
"label": { "label": {
"title": "(fr)Support", "title": "Support",
"howToUse": "(fr)How to use the system", "howToUse": "Comment utiliser le système",
"supportPageEnglish": "OMDS Cloud User Guide", "supportPageEnglish": "OMDS Cloud User Guide",
"supportPageGerman": "OMDS Cloud Benutzerhandbuch", "supportPageGerman": "OMDS Cloud-Benutzerhandbuch",
"supportPageFrench": "OMDS Cloud Mode d'emploi", "supportPageFrench": "Guía del usuario de la nube OMDS",
"supportPageSpanish": "OMDS Cloud Guía del usuario" "supportPageSpanish": "Guide de l'utilisateur du cloud OMDS"
}, },
"text": { "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": { "filePropertyPopup": {
"label": { "label": {
"general": "(fr)General", "general": "Général",
"job": "(fr)Job", "job": "Tâches",
"close": "(fr)Close" "close": "Fermer"
} }
} }
} }

View File

@ -16,7 +16,7 @@ MAIL_FROM=xxxxx@xxxxx.xxxx
NOTIFICATION_HUB_NAME=ntf-odms-dev NOTIFICATION_HUB_NAME=ntf-odms-dev
NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX
APP_DOMAIN=http://localhost:8081/ APP_DOMAIN=http://localhost:8081/
STORAGE_TOKEN_EXPIRE_TIME=30 STORAGE_TOKEN_EXPIRE_TIME=2
STORAGE_ACCOUNT_NAME_US=saodmsusdev STORAGE_ACCOUNT_NAME_US=saodmsusdev
STORAGE_ACCOUNT_NAME_AU=saodmsaudev STORAGE_ACCOUNT_NAME_AU=saodmsaudev
STORAGE_ACCOUNT_NAME_EU=saodmseudev STORAGE_ACCOUNT_NAME_EU=saodmseudev
@ -26,10 +26,10 @@ STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX
STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA
STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA
STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA
ACCESS_TOKEN_LIFETIME_WEB=7200000 ACCESS_TOKEN_LIFETIME_WEB=7200
REFRESH_TOKEN_LIFETIME_WEB=86400000 REFRESH_TOKEN_LIFETIME_WEB=86400
REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000 REFRESH_TOKEN_LIFETIME_DEFAULT=2592000
EMAIL_CONFIRM_LIFETIME=86400000 EMAIL_CONFIRM_LIFETIME=86400
REDIS_HOST=redis-cache REDIS_HOST=redis-cache
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_PASSWORD=omdsredispass REDIS_PASSWORD=omdsredispass

View File

@ -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`;

View File

@ -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`;

View File

@ -0,0 +1,5 @@
-- +migrate Up
ALTER TABLE `users` ADD INDEX `idx_role` (role);
-- +migrate Down
ALTER TABLE `users` DROP INDEX `idx_role`;

View File

@ -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`;

View File

@ -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`;

View File

@ -59,6 +59,7 @@ export const ErrorCodes = [
'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) 'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
'E010812', // ライセンス未割当エラー 'E010812', // ライセンス未割当エラー
'E010908', // タイピストグループ不在エラー 'E010908', // タイピストグループ不在エラー
'E010909', // タイピストグループ名重複エラー
'E011001', // ワークタイプ重複エラー 'E011001', // ワークタイプ重複エラー
'E011002', // ワークタイプ登録上限超過エラー 'E011002', // ワークタイプ登録上限超過エラー
'E011003', // ワークタイプ不在エラー 'E011003', // ワークタイプ不在エラー

View File

@ -48,6 +48,7 @@ export const errors: Errors = {
E010811: 'Already license allocated Error', E010811: 'Already license allocated Error',
E010812: 'License not allocated Error', E010812: 'License not allocated Error',
E010908: 'Typist Group not exist Error', E010908: 'Typist Group not exist Error',
E010909: 'Typist Group name already exist Error',
E011001: 'This WorkTypeID already used Error', E011001: 'This WorkTypeID already used Error',
E011002: 'WorkTypeID create limit exceeded Error', E011002: 'WorkTypeID create limit exceeded Error',
E011003: 'WorkTypeID not found Error', E011003: 'WorkTypeID not found Error',

View File

@ -82,7 +82,8 @@ export const overrideSendgridService = <TService>(
overrides: { overrides: {
sendMail?: ( sendMail?: (
context: Context, context: Context,
to: string, to: string[],
cc: string[],
from: string, from: string,
subject: string, subject: string,
text: string, text: string,

View File

@ -219,9 +219,9 @@ export const PNS = {
}; };
/** /**
* *
*/ */
export const USER_LICENSE_STATUS = { export const USER_LICENSE_EXPIRY_STATUS = {
NORMAL: 'Normal', NORMAL: 'Normal',
NO_LICENSE: 'NoLicense', NO_LICENSE: 'NoLicense',
ALERT: 'Alert', ALERT: 'Alert',
@ -311,3 +311,13 @@ export const USER_AUDIO_FORMAT = 'DS2(QP)';
* @const {string[]} * @const {string[]}
*/ */
export const NODE_ENV_TEST = 'test'; export const NODE_ENV_TEST = 'test';
/**
*
* @const {string[]}
*/
export const USER_LICENSE_STATUS = {
UNALLOCATED: 'unallocated',
ALLOCATED: 'allocated',
EXPIRED: 'expired',
} as const;

View File

@ -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, { overrideBlobstorageService(service, {
createContainer: async () => { createContainer: async () => {
return; return;
@ -175,6 +194,10 @@ describe('createAccount', () => {
expect(user?.accepted_dpa_version).toBe(acceptedDpaVersion); expect(user?.accepted_dpa_version).toBe(acceptedDpaVersion);
expect(user?.account_id).toBe(accountId); expect(user?.account_id).toBe(accountId);
expect(user?.role).toBe(role); 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 () => { it('アカウントを作成がAzure AD B2Cへの通信失敗によって失敗すると500エラーが発生する', async () => {
@ -5704,9 +5727,39 @@ describe('アカウント情報更新', () => {
const module = await makeTestingModule(source); const module = await makeTestingModule(source);
if (!module) fail(); if (!module) fail();
const service = module.get<AccountsService>(AccountsService); const service = module.get<AccountsService>(AccountsService);
let _subject: string = "";
let _url: string | undefined = "";
overrideSendgridService(service, { overrideSendgridService(service, {
sendMail: async () => { sendMail: async (
return; 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?.delegation_permission).toBe(true);
expect(account?.primary_admin_user_id).toBe(tier5Accounts.admin.id); expect(account?.primary_admin_user_id).toBe(tier5Accounts.admin.id);
expect(account?.secondary_admin_user_id).toBe(null); expect(account?.secondary_admin_user_id).toBe(null);
// 想定通りのメールが送られているか確認
expect(_subject).toBe('Account Edit Notification [U-112]');
expect(_url).toBe('http://localhost:8081/');
}); });
it('アカウント情報を更新する(第五階層以外が実行)', async () => { it('アカウント情報を更新する(第五階層以外が実行)', async () => {
if (!source) fail(); if (!source) fail();
@ -6364,7 +6420,27 @@ describe('deleteAccountAndData', () => {
const module = await makeTestingModule(source); const module = await makeTestingModule(source);
if (!module) fail(); if (!module) fail();
const service = module.get<AccountsService>(AccountsService); const service = module.get<AccountsService>(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 { const {
tier1Accounts: tier1Accounts, tier1Accounts: tier1Accounts,
@ -6485,10 +6561,36 @@ describe('deleteAccountAndData', () => {
licensesB[0].id, licensesB[0].id,
); );
// ADB2Cユーザーの削除成功
overrideAdB2cService(service, { 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(), deleteUsers: jest.fn(),
}); });
// blobstorageコンテナの削除成功 // blobstorageコンテナの削除成功
overrideBlobstorageService(service, { overrideBlobstorageService(service, {
deleteContainer: jest.fn(), deleteContainer: jest.fn(),
@ -6559,6 +6661,9 @@ describe('deleteAccountAndData', () => {
const LicenseAllocationHistoryArchive = const LicenseAllocationHistoryArchive =
await getLicenseAllocationHistoryArchive(source); await getLicenseAllocationHistoryArchive(source);
expect(LicenseAllocationHistoryArchive.length).toBe(1); expect(LicenseAllocationHistoryArchive.length).toBe(1);
expect(_subject).toBe('Account Deleted Notification [U-111]');
expect(_url).toBe('http://localhost:8081/');
}); });
it('アカウントの削除に失敗した場合はエラーを返す', async () => { it('アカウントの削除に失敗した場合はエラーを返す', async () => {
if (!source) fail(); if (!source) fail();

View File

@ -60,6 +60,7 @@ import {
} from '../../repositories/licenses/errors/types'; } from '../../repositories/licenses/errors/types';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { import {
TypistGroupNameAlreadyExistError,
TypistGroupNotExistError, TypistGroupNotExistError,
TypistIdInvalidError, TypistIdInvalidError,
} from '../../repositories/user_groups/errors/types'; } from '../../repositories/user_groups/errors/types';
@ -1241,6 +1242,12 @@ export class AccountsService {
makeErrorResponse('E010204'), makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
); );
// 同名のタイピストグループが存在する場合は400エラーを返す
case TypistGroupNameAlreadyExistError:
throw new HttpException(
makeErrorResponse('E010909'),
HttpStatus.BAD_REQUEST,
);
default: default:
throw new HttpException( throw new HttpException(
makeErrorResponse('E009999'), makeErrorResponse('E009999'),
@ -1315,6 +1322,12 @@ export class AccountsService {
makeErrorResponse('E010908'), makeErrorResponse('E010908'),
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
); );
// 同名のタイピストグループが存在する場合は400エラーを返す
case TypistGroupNameAlreadyExistError:
throw new HttpException(
makeErrorResponse('E010909'),
HttpStatus.BAD_REQUEST,
);
default: default:
throw new HttpException( throw new HttpException(
makeErrorResponse('E009999'), makeErrorResponse('E009999'),

View File

@ -198,7 +198,7 @@ export class CancelIssueRequest {
export class CreateWorktypesRequest { export class CreateWorktypesRequest {
@ApiProperty({ minLength: 1, maxLength: 255, description: 'WorktypeID' }) @ApiProperty({ minLength: 1, maxLength: 255, description: 'WorktypeID' })
@MinLength(1) @MinLength(1)
@MaxLength(255) @MaxLength(16)
@IsRecorderAllowed() @IsRecorderAllowed()
worktypeId: string; worktypeId: string;
@ApiProperty({ description: 'Worktypeの説明', required: false }) @ApiProperty({ description: 'Worktypeの説明', required: false })
@ -210,7 +210,7 @@ export class CreateWorktypesRequest {
export class UpdateWorktypesRequest { export class UpdateWorktypesRequest {
@ApiProperty({ minLength: 1, description: 'WorktypeID' }) @ApiProperty({ minLength: 1, description: 'WorktypeID' })
@MinLength(1) @MinLength(1)
@MaxLength(255) @MaxLength(16)
@IsRecorderAllowed() @IsRecorderAllowed()
worktypeId: string; worktypeId: string;
@ApiProperty({ description: 'Worktypeの説明', required: false }) @ApiProperty({ description: 'Worktypeの説明', required: false })

View File

@ -235,67 +235,6 @@ describe('publishUploadSas', () => {
new HttpException(makeErrorResponse('E010812'), HttpStatus.BAD_REQUEST), 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>(FilesService);
await expect(
service.publishUploadSas(
makeContext('trackingId', 'requestId'),
externalId,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST),
);
});
}); });
describe('タスク作成から自動ルーティング(DB使用)', () => { describe('タスク作成から自動ルーティング(DB使用)', () => {
@ -1097,76 +1036,6 @@ describe('音声ファイルダウンロードURL取得', () => {
), ),
).toEqual(`${url}?sas-token`); ).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>(FilesService);
expect(
await service.publishAudioFileDownloadSas(
makeContext('trackingId', 'requestId'),
externalId,
audioFileId,
),
).toEqual(`${url}?sas-token`);
});
it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => { it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
if (!source) fail(); if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source); const { id: accountId } = await makeTestSimpleAccount(source);
@ -1396,133 +1265,6 @@ describe('音声ファイルダウンロードURL取得', () => {
new HttpException(makeErrorResponse('E010701'), HttpStatus.BAD_REQUEST), 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>(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>(FilesService);
await expect(
service.publishAudioFileDownloadSas(
makeContext('trackingId', 'requestId'),
externalId,
audioFileId,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST),
);
});
}); });
describe('テンプレートファイルダウンロードURL取得', () => { describe('テンプレートファイルダウンロードURL取得', () => {
@ -1596,70 +1338,7 @@ describe('テンプレートファイルダウンロードURL取得', () => {
); );
expect(resultUrl).toBe(`${url}?sas-token`); 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>(FilesService);
const resultUrl = await service.publishTemplateFileDownloadSas(
makeContext('trackingId', 'requestId'),
externalId,
audioFileId,
);
expect(resultUrl).toBe(`${url}?sas-token`);
});
it('タスクのステータスが[Inprogress,Pending]以外でエラー', async () => { it('タスクのステータスが[Inprogress,Pending]以外でエラー', async () => {
if (!source) fail(); if (!source) fail();
const { id: accountId } = await makeTestSimpleAccount(source); 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>(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>(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', () => { describe('publishTemplateFileUploadSas', () => {

View File

@ -8,6 +8,7 @@ import {
OPTION_ITEM_NUM, OPTION_ITEM_NUM,
TASK_STATUS, TASK_STATUS,
TIERS, TIERS,
USER_LICENSE_STATUS,
USER_ROLES, USER_ROLES,
} from '../../constants/index'; } from '../../constants/index';
import { User } from '../../repositories/users/entity/user.entity'; import { User } from '../../repositories/users/entity/user.entity';
@ -308,10 +309,10 @@ export class FilesService {
context, context,
user.id, user.id,
); );
if (state === 'expired') { if (state === USER_LICENSE_STATUS.EXPIRED) {
throw new LicenseExpiredError('license is expired.'); throw new LicenseExpiredError('license is expired.');
} }
if (state === 'inallocated') { if (state === USER_LICENSE_STATUS.UNALLOCATED) {
throw new LicenseNotAllocatedError('license is not allocated.'); throw new LicenseNotAllocatedError('license is not allocated.');
} }
} }
@ -392,20 +393,6 @@ export class FilesService {
if (!user.account) { if (!user.account) {
throw new AccountNotFoundError('account not found.'); 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; accountId = user.account.id;
userId = user.id; userId = user.id;
country = user.account.country; country = user.account.country;
@ -422,16 +409,6 @@ export class FilesService {
}`, }`,
); );
switch (e.constructor) { switch (e.constructor) {
case LicenseExpiredError:
throw new HttpException(
makeErrorResponse('E010805'),
HttpStatus.BAD_REQUEST,
);
case LicenseNotAllocatedError:
throw new HttpException(
makeErrorResponse('E010812'),
HttpStatus.BAD_REQUEST,
);
default: default:
throw new HttpException( throw new HttpException(
makeErrorResponse('E009999'), makeErrorResponse('E009999'),
@ -571,20 +548,6 @@ export class FilesService {
if (!user.account) { if (!user.account) {
throw new AccountNotFoundError('account not found.'); 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; accountId = user.account_id;
userId = user.id; userId = user.id;
country = user.account.country; country = user.account.country;
@ -596,16 +559,6 @@ export class FilesService {
}`, }`,
); );
switch (e.constructor) { switch (e.constructor) {
case LicenseExpiredError:
throw new HttpException(
makeErrorResponse('E010805'),
HttpStatus.BAD_REQUEST,
);
case LicenseNotAllocatedError:
throw new HttpException(
makeErrorResponse('E010812'),
HttpStatus.BAD_REQUEST,
);
default: default:
throw new HttpException( throw new HttpException(
makeErrorResponse('E009999'), makeErrorResponse('E009999'),

View File

@ -17,15 +17,15 @@ import {
selectOrderLicense, selectOrderLicense,
} from './test/utility'; } from './test/utility';
import { UsersService } from '../users/users.service'; import { UsersService } from '../users/users.service';
import { makeContext } from '../../common/log'; import { Context, makeContext } from '../../common/log';
import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants'; import { ADB2C_SIGN_IN_TYPE, LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants';
import { import {
makeHierarchicalAccounts, makeHierarchicalAccounts,
makeTestSimpleAccount, makeTestSimpleAccount,
makeTestUser, makeTestUser,
} from '../../common/test/utility'; } from '../../common/test/utility';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; 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'; import { truncateAllTable } from '../../common/test/init';
describe('ライセンス注文', () => { describe('ライセンス注文', () => {
@ -192,8 +192,8 @@ describe('ライセンス注文', () => {
await service.licenseOrders(context, externalId, poNumber, orderCount); await service.licenseOrders(context, externalId, poNumber, orderCount);
} catch (e) { } catch (e) {
if (e instanceof HttpException) { if (e instanceof HttpException) {
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); expect(e.getResponse()).toEqual(makeErrorResponse('E010501'));
} else { } else {
fail(); fail();
} }
@ -672,7 +672,18 @@ describe('ライセンス割り当て', () => {
const module = await makeTestingModule(source); const module = await makeTestingModule(source);
if (!module) fail(); 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, { const { id: userId } = await makeTestUser(source, {
account_id: accountId, account_id: accountId,
external_id: 'userId', external_id: 'userId',
@ -701,7 +712,55 @@ describe('ライセンス割り当て', () => {
); );
const service = module.get<UsersService>(UsersService); const service = module.get<UsersService>(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(); const expiry_date = new NewAllocatedLicenseExpirationDate();
@ -735,6 +794,9 @@ describe('ライセンス割り当て', () => {
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe( expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
accountId, accountId,
); );
expect(_subject).toBe('License Assigned Notification [U-108]');
expect(_url).toBe('http://localhost:8081/');
}); });
it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => { it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => {

View File

@ -80,7 +80,9 @@ export class LicensesService {
.parent_account_id ?? undefined; .parent_account_id ?? undefined;
// 親アカウントIDが取得できない場合はエラー // 親アカウントIDが取得できない場合はエラー
if (parentAccountId === undefined) { if (parentAccountId === undefined) {
throw new Error('parent account id is undefined'); throw new AccountNotFoundError(
`parent account id is not found. myAccountId: ${myAccountId}`,
);
} }
} catch (e) { } catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error(`[${context.getTrackingId()}] error=${e}`);
@ -147,6 +149,7 @@ export class LicensesService {
); );
} }
} }
async issueCardLicenseKeys( async issueCardLicenseKeys(
context: Context, context: Context,
externalId: string, externalId: string,

View File

@ -8,6 +8,7 @@ import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_
import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module'; import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module';
import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module';
import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module'; import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module';
import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module';
@Module({ @Module({
imports: [ imports: [
@ -18,6 +19,7 @@ import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.r
AdB2cModule, AdB2cModule,
NotificationhubModule, NotificationhubModule,
SendGridModule, SendGridModule,
LicensesRepositoryModule,
], ],
providers: [TasksService], providers: [TasksService],
controllers: [TasksController], controllers: [TasksController],

View File

@ -25,7 +25,13 @@ import {
makeTestSimpleAccount, makeTestSimpleAccount,
makeTestUser, makeTestUser,
} from '../../common/test/utility'; } 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 { makeTestingModule } from '../../common/test/modules';
import { createSortCriteria } from '../users/test/utility'; import { createSortCriteria } from '../users/test/utility';
import { createWorktype } from '../accounts/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 { Roles } from '../../common/types/role';
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
import { truncateAllTable } from '../../common/test/init'; 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', () => { describe('TasksService', () => {
it('タスク一覧を取得できるadmin', async () => { it('タスク一覧を取得できるadmin', async () => {
@ -48,12 +57,15 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue = const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue(); makeDefaultNotificationhubServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeTasksServiceMock( const service = await makeTasksServiceMock(
tasksRepositoryMockValue, tasksRepositoryMockValue,
usersRepositoryMockValue, usersRepositoryMockValue,
userGroupsRepositoryMockValue, userGroupsRepositoryMockValue,
adb2cServiceMockValue, adb2cServiceMockValue,
notificationhubServiceMockValue, notificationhubServiceMockValue,
licensesRepositoryMockValue,
); );
const userId = 'userId'; const userId = 'userId';
@ -122,6 +134,8 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue = const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue(); makeDefaultNotificationhubServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
usersRepositoryMockValue.findUserByExternalId = new Error('DB failed'); usersRepositoryMockValue.findUserByExternalId = new Error('DB failed');
const service = await makeTasksServiceMock( const service = await makeTasksServiceMock(
tasksRepositoryMockValue, tasksRepositoryMockValue,
@ -129,6 +143,7 @@ describe('TasksService', () => {
userGroupsRepositoryMockValue, userGroupsRepositoryMockValue,
adb2cServiceMockValue, adb2cServiceMockValue,
notificationhubServiceMockValue, notificationhubServiceMockValue,
licensesRepositoryMockValue,
); );
const userId = 'userId'; const userId = 'userId';
@ -164,6 +179,8 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue = const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue(); makeDefaultNotificationhubServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
tasksRepositoryMockValue.getTasksFromAccountId = new Error('DB failed'); tasksRepositoryMockValue.getTasksFromAccountId = new Error('DB failed');
const service = await makeTasksServiceMock( const service = await makeTasksServiceMock(
tasksRepositoryMockValue, tasksRepositoryMockValue,
@ -171,6 +188,7 @@ describe('TasksService', () => {
userGroupsRepositoryMockValue, userGroupsRepositoryMockValue,
adb2cServiceMockValue, adb2cServiceMockValue,
notificationhubServiceMockValue, notificationhubServiceMockValue,
licensesRepositoryMockValue,
); );
const userId = 'userId'; const userId = 'userId';
@ -252,12 +270,15 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue = const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue(); makeDefaultNotificationhubServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeTasksServiceMock( const service = await makeTasksServiceMock(
tasksRepositoryMockValue, tasksRepositoryMockValue,
usersRepositoryMockValue, usersRepositoryMockValue,
userGroupsRepositoryMockValue, userGroupsRepositoryMockValue,
adb2cServiceMockValue, adb2cServiceMockValue,
notificationhubServiceMockValue, notificationhubServiceMockValue,
licensesRepositoryMockValue,
); );
const userId = 'userId'; const userId = 'userId';
const offset = 0; const offset = 0;
@ -292,6 +313,8 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue = const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue(); makeDefaultNotificationhubServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
if (usersRepositoryMockValue.findUserByExternalId instanceof Error) { if (usersRepositoryMockValue.findUserByExternalId instanceof Error) {
return; return;
} }
@ -302,6 +325,7 @@ describe('TasksService', () => {
userGroupsRepositoryMockValue, userGroupsRepositoryMockValue,
adb2cServiceMockValue, adb2cServiceMockValue,
notificationhubServiceMockValue, notificationhubServiceMockValue,
licensesRepositoryMockValue,
); );
const userId = 'userId'; const userId = 'userId';
@ -376,6 +400,8 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue = const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue(); makeDefaultNotificationhubServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
tasksRepositoryMockValue.getTasksFromAuthorIdAndAccountId = new Error( tasksRepositoryMockValue.getTasksFromAuthorIdAndAccountId = new Error(
'DB failed', 'DB failed',
); );
@ -385,6 +411,7 @@ describe('TasksService', () => {
userGroupsRepositoryMockValue, userGroupsRepositoryMockValue,
adb2cServiceMockValue, adb2cServiceMockValue,
notificationhubServiceMockValue, notificationhubServiceMockValue,
licensesRepositoryMockValue,
); );
const userId = 'userId'; const userId = 'userId';
@ -420,6 +447,8 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue = const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue(); makeDefaultNotificationhubServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
if (usersRepositoryMockValue.findUserByExternalId instanceof Error) { if (usersRepositoryMockValue.findUserByExternalId instanceof Error) {
return; return;
} }
@ -431,6 +460,7 @@ describe('TasksService', () => {
userGroupsRepositoryMockValue, userGroupsRepositoryMockValue,
adb2cServiceMockValue, adb2cServiceMockValue,
notificationhubServiceMockValue, notificationhubServiceMockValue,
licensesRepositoryMockValue,
); );
const userId = 'userId'; const userId = 'userId';
@ -508,12 +538,15 @@ describe('TasksService', () => {
tasksRepositoryMockValue.getTasksFromTypistRelations = new Error( tasksRepositoryMockValue.getTasksFromTypistRelations = new Error(
'DB failed', 'DB failed',
); );
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
const service = await makeTasksServiceMock( const service = await makeTasksServiceMock(
tasksRepositoryMockValue, tasksRepositoryMockValue,
usersRepositoryMockValue, usersRepositoryMockValue,
userGroupsRepositoryMockValue, userGroupsRepositoryMockValue,
adb2cServiceMockValue, adb2cServiceMockValue,
notificationhubServiceMockValue, notificationhubServiceMockValue,
licensesRepositoryMockValue,
); );
const userId = 'userId'; const userId = 'userId';
@ -549,6 +582,8 @@ describe('TasksService', () => {
const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue();
const notificationhubServiceMockValue = const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue(); makeDefaultNotificationhubServiceMockValue();
const licensesRepositoryMockValue =
makeDefaultLicensesRepositoryMockValue();
adb2cServiceMockValue.getUsers = new Adb2cTooManyRequestsError(); adb2cServiceMockValue.getUsers = new Adb2cTooManyRequestsError();
const service = await makeTasksServiceMock( const service = await makeTasksServiceMock(
tasksRepositoryMockValue, tasksRepositoryMockValue,
@ -556,6 +591,7 @@ describe('TasksService', () => {
userGroupsRepositoryMockValue, userGroupsRepositoryMockValue,
adb2cServiceMockValue, adb2cServiceMockValue,
notificationhubServiceMockValue, notificationhubServiceMockValue,
licensesRepositoryMockValue,
); );
const userId = 'userId'; const userId = 'userId';
@ -1632,7 +1668,75 @@ describe('checkout', () => {
user_group_id: null, 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>(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 () => { it('ユーザーのRoleがTypistで、対象のタスクのStatus[Uploaded,Inprogress,Pending]以外の時、タスクをチェックアウトできない', async () => {
if (!source) fail(); if (!source) fail();
const module = await makeTestingModule(source); 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>(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>(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 () => { it('ユーザーのRoleがTypistで、チェックアウト権限が存在しない時、タスクをチェックアウトできない', async () => {
if (!source) fail(); if (!source) fail();
const module = await makeTestingModule(source); const module = await makeTestingModule(source);

View File

@ -9,7 +9,13 @@ import {
SortDirection, SortDirection,
TaskListSortableAttribute, TaskListSortableAttribute,
} from '../../common/types/sort'; } 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 { import {
AdB2cService, AdB2cService,
Adb2cTooManyRequestsError, Adb2cTooManyRequestsError,
@ -36,6 +42,12 @@ import { User } from '../../repositories/users/entity/user.entity';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; 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() @Injectable()
export class TasksService { export class TasksService {
@ -48,6 +60,7 @@ export class TasksService {
private readonly adB2cService: AdB2cService, private readonly adB2cService: AdB2cService,
private readonly sendgridService: SendGridService, private readonly sendgridService: SendGridService,
private readonly notificationhubService: NotificationhubService, private readonly notificationhubService: NotificationhubService,
private readonly licensesRepository: LicensesRepositoryService,
) {} ) {}
async getTasks( async getTasks(
@ -276,9 +289,26 @@ export class TasksService {
} | params: { audioFileId: ${audioFileId}, roles: ${roles}, externalId: ${externalId} };`, } | 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); 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)) { if (roles.includes(USER_ROLES.AUTHOR)) {
// API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする // API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
if (!author_id) { if (!author_id) {
@ -308,6 +338,16 @@ export class TasksService {
this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) { if (e instanceof Error) {
switch (e.constructor) { 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 CheckoutPermissionNotFoundError:
case TaskAuthorIdNotMatchError: case TaskAuthorIdNotMatchError:
case InvalidRoleError: case InvalidRoleError:

View File

@ -17,6 +17,11 @@ import { NotificationhubService } from '../../../gateways/notificationhub/notifi
import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service'; import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service';
import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service'; import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service';
import { SendGridService } from '../../../gateways/sendgrid/sendgrid.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 = { export type TasksRepositoryMockValue = {
getTasksFromAccountId: getTasksFromAccountId:
@ -65,6 +70,7 @@ export const makeTasksServiceMock = async (
userGroupsRepositoryMockValue: UserGroupsRepositoryMockValue, userGroupsRepositoryMockValue: UserGroupsRepositoryMockValue,
adB2CServiceMockValue: AdB2CServiceMockValue, adB2CServiceMockValue: AdB2CServiceMockValue,
notificationhubServiceMockValue: NotificationhubServiceMockValue, notificationhubServiceMockValue: NotificationhubServiceMockValue,
licensesRepositoryMockValue: LicensesRepositoryMockValue,
): Promise<{ ): Promise<{
tasksService: TasksService; tasksService: TasksService;
taskRepoService: TasksRepositoryService; taskRepoService: TasksRepositoryService;
@ -92,6 +98,8 @@ export const makeTasksServiceMock = async (
// メール送信でしか利用しておらず、テストする必要がないが、依存関係解決のため空オブジェクトを定義しておく。 // メール送信でしか利用しておらず、テストする必要がないが、依存関係解決のため空オブジェクトを定義しておく。
case SendGridService: case SendGridService:
return {}; return {};
case LicensesRepositoryService:
return makeLicensesRepositoryMock(licensesRepositoryMockValue);
} }
}) })
.compile(); .compile();

View File

@ -9,7 +9,7 @@ import {
} from 'class-validator'; } from 'class-validator';
import { import {
TASK_LIST_SORTABLE_ATTRIBUTES, TASK_LIST_SORTABLE_ATTRIBUTES,
USER_LICENSE_STATUS, USER_LICENSE_EXPIRY_STATUS,
} from '../../../constants'; } from '../../../constants';
import { USER_ROLES } from '../../../constants'; import { USER_ROLES } from '../../../constants';
import { import {
@ -67,9 +67,9 @@ export class User {
remaining?: number; remaining?: number;
@ApiProperty({ @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', message: 'invalid license status',
}) })
licenseStatus: string; licenseStatus: string;

View File

@ -22,7 +22,7 @@ import {
LICENSE_EXPIRATION_THRESHOLD_DAYS, LICENSE_EXPIRATION_THRESHOLD_DAYS,
LICENSE_TYPE, LICENSE_TYPE,
USER_AUDIO_FORMAT, USER_AUDIO_FORMAT,
USER_LICENSE_STATUS, USER_LICENSE_EXPIRY_STATUS,
USER_ROLES, USER_ROLES,
} from '../../constants'; } from '../../constants';
import { makeTestingModule } from '../../common/test/modules'; import { makeTestingModule } from '../../common/test/modules';
@ -108,7 +108,26 @@ describe('UsersService.confirmUser', () => {
}); });
const service = module.get<UsersService>(UsersService); const service = module.get<UsersService>(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のトークン // account id:1, user id: 2のトークン
const token = const token =
@ -149,6 +168,8 @@ describe('UsersService.confirmUser', () => {
delete_order_id: null, delete_order_id: null,
user: null, user: null,
}); });
expect(_subject).toBe('Account Registered Notification [U-101]');
expect(_url).toBe('http://localhost:8081/');
}, 600000); }, 600000);
it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => { 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( expect(
await service.createUser( await service.createUser(
@ -536,6 +576,11 @@ describe('UsersService.createUser', () => {
// 他にユーザーが登録されていないことを確認 // 他にユーザーが登録されていないことを確認
const users = await getUsers(source); const users = await getUsers(source);
expect(users.length).toEqual(2); 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 () => { it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化あり)', async () => {
@ -1479,7 +1524,7 @@ describe('UsersService.getUsers', () => {
prompt: false, prompt: false,
expiration: undefined, expiration: undefined,
remaining: undefined, remaining: undefined,
licenseStatus: USER_LICENSE_STATUS.NO_LICENSE, licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
}, },
{ {
id: typistUserId, id: typistUserId,
@ -1495,7 +1540,7 @@ describe('UsersService.getUsers', () => {
prompt: false, prompt: false,
expiration: undefined, expiration: undefined,
remaining: undefined, remaining: undefined,
licenseStatus: USER_LICENSE_STATUS.NO_LICENSE, licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
}, },
{ {
id: noneUserId, id: noneUserId,
@ -1511,7 +1556,7 @@ describe('UsersService.getUsers', () => {
prompt: false, prompt: false,
expiration: undefined, expiration: undefined,
remaining: 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.getMonth() + 1
}/${date1.getDate()}`, }/${date1.getDate()}`,
remaining: LICENSE_EXPIRATION_THRESHOLD_DAYS + 1, remaining: LICENSE_EXPIRATION_THRESHOLD_DAYS + 1,
licenseStatus: USER_LICENSE_STATUS.NORMAL, licenseStatus: USER_LICENSE_EXPIRY_STATUS.NORMAL,
}, },
{ {
id: user2, id: user2,
@ -1609,7 +1654,7 @@ describe('UsersService.getUsers', () => {
date2.getMonth() + 1 date2.getMonth() + 1
}/${date2.getDate()}`, }/${date2.getDate()}`,
remaining: LICENSE_EXPIRATION_THRESHOLD_DAYS, remaining: LICENSE_EXPIRATION_THRESHOLD_DAYS,
licenseStatus: USER_LICENSE_STATUS.RENEW, licenseStatus: USER_LICENSE_EXPIRY_STATUS.RENEW,
}, },
{ {
id: user3, id: user3,
@ -1627,7 +1672,7 @@ describe('UsersService.getUsers', () => {
date3.getMonth() + 1 date3.getMonth() + 1
}/${date3.getDate()}`, }/${date3.getDate()}`,
remaining: LICENSE_EXPIRATION_THRESHOLD_DAYS - 1, remaining: LICENSE_EXPIRATION_THRESHOLD_DAYS - 1,
licenseStatus: USER_LICENSE_STATUS.ALERT, licenseStatus: USER_LICENSE_EXPIRY_STATUS.ALERT,
}, },
]; ];

View File

@ -37,7 +37,7 @@ import {
MANUAL_RECOVERY_REQUIRED, MANUAL_RECOVERY_REQUIRED,
OPTION_ITEM_VALUE_TYPE_NUMBER, OPTION_ITEM_VALUE_TYPE_NUMBER,
USER_AUDIO_FORMAT, USER_AUDIO_FORMAT,
USER_LICENSE_STATUS, USER_LICENSE_EXPIRY_STATUS,
USER_ROLES, USER_ROLES,
} from '../../constants'; } from '../../constants';
import { DateWithZeroTime } from '../licenses/types/types'; import { DateWithZeroTime } from '../licenses/types/types';
@ -617,7 +617,7 @@ export class UsersService {
throw new Error('mail not found.'); throw new Error('mail not found.');
} }
let status = USER_LICENSE_STATUS.NORMAL; let status = USER_LICENSE_EXPIRY_STATUS.NORMAL;
// ライセンスの有効期限と残日数は、ライセンスが存在する場合のみ算出する // ライセンスの有効期限と残日数は、ライセンスが存在する場合のみ算出する
// ライセンスが存在しない場合は、undefinedのままとする // ライセンスが存在しない場合は、undefinedのままとする
@ -648,11 +648,11 @@ export class UsersService {
remaining <= LICENSE_EXPIRATION_THRESHOLD_DAYS remaining <= LICENSE_EXPIRATION_THRESHOLD_DAYS
) { ) {
status = dbUser.auto_renew status = dbUser.auto_renew
? USER_LICENSE_STATUS.RENEW ? USER_LICENSE_EXPIRY_STATUS.RENEW
: USER_LICENSE_STATUS.ALERT; : USER_LICENSE_EXPIRY_STATUS.ALERT;
} }
} else { } else {
status = USER_LICENSE_STATUS.NO_LICENSE; status = USER_LICENSE_EXPIRY_STATUS.NO_LICENSE;
} }
return { return {

View File

@ -21,6 +21,7 @@ import {
VERIFY_LINK, VERIFY_LINK,
TEMPORARY_PASSWORD, TEMPORARY_PASSWORD,
} from '../../templates/constants'; } from '../../templates/constants';
import { URL } from 'node:url';
@Injectable() @Injectable()
export class SendGridService { export class SendGridService {
@ -204,12 +205,13 @@ export class SendGridService {
); );
try { try {
const subject = 'Account Registered Notification [U-101]'; const subject = 'Account Registered Notification [U-101]';
const url = new URL(this.appDomain).href;
const html = this.templateU101Html const html = this.templateU101Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
const text = this.templateU101Text const text = this.templateU101Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
await this.sendMail( await this.sendMail(
context, context,
@ -255,8 +257,9 @@ export class SendGridService {
this.emailConfirmLifetime, this.emailConfirmLifetime,
privateKey, privateKey,
); );
const path = 'mail-confirm/'; const paths = path.join('mail-confirm');
const verifyUrl = `${this.appDomain}${path}?verify=${token}`; const url = new URL(paths, this.appDomain).href;
const verifyUrl = `${url}?verify=${token}`;
const subject = 'User Registration Notification [U-102]'; const subject = 'User Registration Notification [U-102]';
const html = this.templateU102Html.replaceAll(VERIFY_LINK, verifyUrl); const html = this.templateU102Html.replaceAll(VERIFY_LINK, verifyUrl);
@ -466,6 +469,7 @@ export class SendGridService {
); );
try { try {
const subject = 'License Assigned Notification [U-108]'; const subject = 'License Assigned Notification [U-108]';
const url = new URL(this.appDomain).href;
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU108Html const html = this.templateU108Html
@ -473,13 +477,13 @@ export class SendGridService {
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName) .replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail) .replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
const text = this.templateU108Text const text = this.templateU108Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName) .replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail) .replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
const ccAddress = customerAdminMails.includes(userMail) ? [] : [userMail]; const ccAddress = customerAdminMails.includes(userMail) ? [] : [userMail];
@ -574,17 +578,18 @@ export class SendGridService {
); );
try { try {
const subject = 'Account Deleted Notification [U-111]'; const subject = 'Account Deleted Notification [U-111]';
const url = new URL(this.appDomain).href;
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU111Html const html = this.templateU111Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
const text = this.templateU111Text const text = this.templateU111Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(
@ -627,6 +632,7 @@ export class SendGridService {
let html: string; let html: string;
let text: string; let text: string;
const url = new URL(this.appDomain).href;
// 親アカウントがない場合は別のテンプレートを使用する // 親アカウントがない場合は別のテンプレートを使用する
if (dealerAccountName === null) { if (dealerAccountName === null) {
@ -634,22 +640,22 @@ export class SendGridService {
html = this.templateU112NoParentHtml html = this.templateU112NoParentHtml
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
text = this.templateU112NoParentText text = this.templateU112NoParentText
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
} else { } else {
html = this.templateU112Html html = this.templateU112Html
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
text = this.templateU112Text text = this.templateU112Text
.replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, this.appDomain); .replaceAll(TOP_URL, url);
} }
// メールを送信する // メールを送信する
@ -745,18 +751,20 @@ export class SendGridService {
this.emailConfirmLifetime, this.emailConfirmLifetime,
privateKey, 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 subject = 'User Registration Notification [U-114]';
// メールの本文を作成する // メールの本文を作成する
const html = this.templateU114Html const html = this.templateU114Html
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(VERIFY_LINK, verifyLink); .replaceAll(VERIFY_LINK, verifyUrl);
const text = this.templateU114Text const text = this.templateU114Text
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(VERIFY_LINK, verifyLink); .replaceAll(VERIFY_LINK, verifyUrl);
// メールを送信する // メールを送信する
await this.sendMail( await this.sendMail(

View File

@ -821,6 +821,7 @@ export class AccountsRepositoryService {
status: Not(LICENSE_ALLOCATED_STATUS.UNALLOCATED), status: Not(LICENSE_ALLOCATED_STATUS.UNALLOCATED),
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// 存在した場合エラー // 存在した場合エラー
@ -1023,6 +1024,7 @@ export class AccountsRepositoryService {
email_verified: true, email_verified: true,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!primaryAdminUser) { if (!primaryAdminUser) {
throw new AdminUserNotFoundError( throw new AdminUserNotFoundError(
@ -1040,6 +1042,7 @@ export class AccountsRepositoryService {
email_verified: true, email_verified: true,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!secondryAdminUser) { if (!secondryAdminUser) {
throw new AdminUserNotFoundError( throw new AdminUserNotFoundError(

View File

@ -12,9 +12,9 @@ import {
LICENSE_ALLOCATED_STATUS, LICENSE_ALLOCATED_STATUS,
LICENSE_ISSUE_STATUS, LICENSE_ISSUE_STATUS,
LICENSE_TYPE, LICENSE_TYPE,
NODE_ENV_TEST,
SWITCH_FROM_TYPE, SWITCH_FROM_TYPE,
TIERS, TIERS,
USER_LICENSE_STATUS,
} from '../../constants'; } from '../../constants';
import { import {
PoNumberAlreadyExistError, PoNumberAlreadyExistError,
@ -39,6 +39,8 @@ import {
updateEntity, updateEntity,
} from '../../common/repository'; } from '../../common/repository';
import { Context } from '../../common/log'; import { Context } from '../../common/log';
import { User } from '../users/entity/user.entity';
import { UserNotFoundError } from '../users/errors/types';
@Injectable() @Injectable()
export class LicensesRepositoryService { export class LicensesRepositoryService {
@ -81,6 +83,7 @@ export class LicensesRepositoryService {
}, },
], ],
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// 重複があった場合はエラーを返却する // 重複があった場合はエラーを返却する
if (isPoNumberDuplicated) { if (isPoNumberDuplicated) {
@ -193,6 +196,7 @@ export class LicensesRepositoryService {
card_license_key: In(generateKeys), card_license_key: In(generateKeys),
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (existingCardLicenses.length > 0) { if (existingCardLicenses.length > 0) {
// 重複分を配列から削除 // 重複分を配列から削除
@ -292,6 +296,7 @@ export class LicensesRepositoryService {
card_license_key: licenseKey, card_license_key: licenseKey,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// カードライセンスが存在しなければエラー // カードライセンスが存在しなければエラー
if (!targetCardLicense) { if (!targetCardLicense) {
@ -422,10 +427,7 @@ export class LicensesRepositoryService {
po_number: poNumber, po_number: poNumber,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
// テスト環境の場合はロックを行わない(sqliteがlockに対応していないため) lock: { mode: 'pessimistic_write' },
...(process.env.NODE_ENV !== NODE_ENV_TEST
? { lock: { mode: 'pessimistic_write' } }
: {}),
}); });
if (!issuingOrder) { if (!issuingOrder) {
// 注文が存在しない場合、エラー // 注文が存在しない場合、エラー
@ -559,6 +561,19 @@ export class LicensesRepositoryService {
accountId: number, accountId: number,
): Promise<void> { ): Promise<void> {
await this.dataSource.transaction(async (entityManager) => { 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 licenseRepo = entityManager.getRepository(License);
const licenseAllocationHistoryRepo = entityManager.getRepository( const licenseAllocationHistoryRepo = entityManager.getRepository(
LicenseAllocationHistory, LicenseAllocationHistory,
@ -569,6 +584,7 @@ export class LicensesRepositoryService {
id: newLicenseId, id: newLicenseId,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// ライセンスが存在しない場合はエラー // ライセンスが存在しない場合はエラー
@ -604,6 +620,7 @@ export class LicensesRepositoryService {
allocated_user_id: userId, allocated_user_id: userId,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// 既にライセンスが割り当てられているなら、割り当てを解除 // 既にライセンスが割り当てられているなら、割り当てを解除
@ -719,6 +736,7 @@ export class LicensesRepositoryService {
status: LICENSE_ALLOCATED_STATUS.ALLOCATED, status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// ライセンスが割り当てられていない場合はエラー // ライセンスが割り当てられていない場合はエラー
@ -778,6 +796,7 @@ export class LicensesRepositoryService {
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING, status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// キャンセル対象の注文が存在しない場合エラー // キャンセル対象の注文が存在しない場合エラー
@ -806,12 +825,17 @@ export class LicensesRepositoryService {
* *
* @param userId ID * @param userId ID
* @error { Error } DBアクセス失敗時の例外 * @error { Error } DBアクセス失敗時の例外
* @returns Promise<{ state: 'allocated' | 'inallocated' | 'expired' }> * @returns Promise<{ state: 'allocated' | 'unallocated' | 'expired' }>
*/ */
async getLicenseState( async getLicenseState(
context: Context, context: Context,
userId: number, 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 const allocatedLicense = await this.dataSource
.getRepository(License) .getRepository(License)
.findOne({ .findOne({
@ -824,7 +848,7 @@ export class LicensesRepositoryService {
// ライセンスが割り当てられていない場合は未割当状態 // ライセンスが割り当てられていない場合は未割当状態
if (allocatedLicense == null) { 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 &&
allocatedLicense.expiry_date < currentDate allocatedLicense.expiry_date < currentDate
) { ) {
return { state: 'expired' }; return { state: USER_LICENSE_STATUS.EXPIRED };
} }
return { state: 'allocated' }; return { state: USER_LICENSE_STATUS.ALLOCATED };
} }
} }

View File

@ -48,6 +48,7 @@ import {
deleteEntity, deleteEntity,
} from '../../common/repository'; } from '../../common/repository';
import { Context } from '../../common/log'; import { Context } from '../../common/log';
import { UserNotFoundError } from '../users/errors/types';
@Injectable() @Injectable()
export class TasksRepositoryService { export class TasksRepositoryService {
@ -167,6 +168,20 @@ export class TasksRepositoryService {
permittedSourceStatus: TaskStatus[], permittedSourceStatus: TaskStatus[],
): Promise<void> { ): Promise<void> {
await this.dataSource.transaction(async (entityManager) => { 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); const taskRepo = entityManager.getRepository(Task);
// 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得 // 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得
const task = await taskRepo.findOne({ const task = await taskRepo.findOne({
@ -174,6 +189,7 @@ export class TasksRepositoryService {
audio_file_id: audio_file_id, audio_file_id: audio_file_id,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!task) { if (!task) {
throw new TasksNotFoundError( throw new TasksNotFoundError(
@ -189,6 +205,7 @@ export class TasksRepositoryService {
typist_user_id: user_id, typist_user_id: user_id,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (tasks.length > 0) { if (tasks.length > 0) {
@ -227,6 +244,7 @@ export class TasksRepositoryService {
}, },
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// ユーザーの所属するすべてのグループIDを列挙 // ユーザーの所属するすべてのグループIDを列挙
const groupIds = groups.map((member) => member.user_group_id); const groupIds = groups.map((member) => member.user_group_id);
@ -249,6 +267,7 @@ export class TasksRepositoryService {
}, },
], ],
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
//チェックアウト権限がなければエラー //チェックアウト権限がなければエラー
@ -319,6 +338,7 @@ export class TasksRepositoryService {
audio_file_id: audio_file_id, audio_file_id: audio_file_id,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!task) { if (!task) {
throw new TasksNotFoundError( throw new TasksNotFoundError(
@ -372,6 +392,7 @@ export class TasksRepositoryService {
audio_file_id: audio_file_id, audio_file_id: audio_file_id,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!task) { if (!task) {
throw new TasksNotFoundError( throw new TasksNotFoundError(
@ -447,6 +468,7 @@ export class TasksRepositoryService {
audio_file_id: audio_file_id, audio_file_id: audio_file_id,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!task) { if (!task) {
throw new TasksNotFoundError( throw new TasksNotFoundError(
@ -498,6 +520,7 @@ export class TasksRepositoryService {
audio_file_id: audio_file_id, audio_file_id: audio_file_id,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!task) { if (!task) {
throw new TasksNotFoundError( throw new TasksNotFoundError(
@ -846,6 +869,22 @@ export class TasksRepositoryService {
const createdEntity = await this.dataSource.transaction( const createdEntity = await this.dataSource.transaction(
async (entityManager) => { 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 audioFileRepo = entityManager.getRepository(AudioFile);
const newAudioFile = audioFileRepo.create(audioFile); const newAudioFile = audioFileRepo.create(audioFile);
const savedAudioFile = await insertEntity( const savedAudioFile = await insertEntity(
@ -865,6 +904,7 @@ export class TasksRepositoryService {
where: { account_id: account_id, is_job_number_enabled: true }, where: { account_id: account_id, is_job_number_enabled: true },
order: { created_at: 'DESC', job_number: 'DESC' }, order: { created_at: 'DESC', job_number: 'DESC' },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
let newJobNumber = '00000001'; let newJobNumber = '00000001';
@ -927,30 +967,6 @@ export class TasksRepositoryService {
assignees: Assignee[], assignees: Assignee[],
): Promise<void> { ): Promise<void> {
await this.dataSource.transaction(async (entityManager) => { 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の取得/存在確認 // Userの取得/存在確認
const typistUserIds = assignees const typistUserIds = assignees
.filter((x) => x.typistUserId !== undefined) .filter((x) => x.typistUserId !== undefined)
@ -967,6 +983,7 @@ export class TasksRepositoryService {
deleted_at: IsNull(), deleted_at: IsNull(),
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// idはユニークであるため取得件数の一致でユーザーの存在を確認 // idはユニークであるため取得件数の一致でユーザーの存在を確認
if (typistUserIds.length !== userRecords.length) { 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レコードを特定し、そのステータスを取得/存在確認 // 引数audioFileIdを使ってTaskレコードを特定し、そのステータスを取得/存在確認
const taskRepo = entityManager.getRepository(Task); const taskRepo = entityManager.getRepository(Task);
@ -992,6 +1034,7 @@ export class TasksRepositoryService {
}, },
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
//タスクが存在しない or ステータスがUploadedでなければエラー //タスクが存在しない or ステータスがUploadedでなければエラー
if (!taskRecord) { if (!taskRecord) {
@ -1155,39 +1198,51 @@ export class TasksRepositoryService {
return await this.dataSource.transaction(async (entityManager) => { return await this.dataSource.transaction(async (entityManager) => {
// 音声ファイルを取得 // 音声ファイルを取得
const audioFileRepo = entityManager.getRepository(AudioFile); const audioFileRepo = entityManager.getRepository(AudioFile);
const audioFile = await audioFileRepo.findOne({ const audio = await audioFileRepo.findOne({
relations: {
task: true,
},
where: { where: {
id: audioFileId, id: audioFileId,
account_id: accountId, account_id: accountId,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
}); });
if (!audioFile) { if (!audio) {
throw new Error( throw new Error(
`audio file not found. audio_file_id:${audioFileId}, accountId:${accountId}`, `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をもとにユーザーを取得 // authorIdをもとにユーザーを取得
const userRepo = entityManager.getRepository(User); const userRepo = entityManager.getRepository(User);
const authorUser = await userRepo.findOne({ const authorUser = await userRepo.findOne({
where: { where: {
author_id: audioFile.author_id, author_id: audio.author_id,
account_id: accountId, account_id: accountId,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, 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を取得 // 音声ファイル上のworktypeIdをもとにworktypeを取得
const worktypeRepo = entityManager.getRepository(Worktype); const worktypeRepo = entityManager.getRepository(Worktype);
const worktypeRecord = await worktypeRepo.findOne({ const worktypeRecord = await worktypeRepo.findOne({
@ -1196,6 +1251,7 @@ export class TasksRepositoryService {
account_id: accountId, account_id: accountId,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// 音声ファイル上のworktypeIdが設定されているが、一致するworktypeが存在しない場合はエラーを出して終了 // 音声ファイル上のworktypeIdが設定されているが、一致するworktypeが存在しない場合はエラーを出して終了
@ -1217,6 +1273,7 @@ export class TasksRepositoryService {
worktype_id: worktypeRecord?.id ?? IsNull(), worktype_id: worktypeRecord?.id ?? IsNull(),
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// Workflowルーティングルールがあればタスクのチェックアウト権限を設定する // Workflowルーティングルールがあればタスクのチェックアウト権限を設定する
@ -1244,6 +1301,7 @@ export class TasksRepositoryService {
account_id: accountId, account_id: accountId,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!myAuthorUser) { if (!myAuthorUser) {
throw new Error( throw new Error(
@ -1260,6 +1318,7 @@ export class TasksRepositoryService {
worktype_id: worktypeRecord?.id ?? IsNull(), worktype_id: worktypeRecord?.id ?? IsNull(),
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// API実行者のAuthorIdと音声ファイルのWorktypeをもとにルーティングルールを取得できない場合はエラーを出して終了 // API実行者のAuthorIdと音声ファイルのWorktypeをもとにルーティングルールを取得できない場合はエラーを出して終了
@ -1326,6 +1385,7 @@ export class TasksRepositoryService {
const typistUsers = await userRepo.find({ const typistUsers = await userRepo.find({
where: { account_id: accountId, id: In(typistIds) }, where: { account_id: accountId, id: In(typistIds) },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (typistUsers.length !== typistIds.length) { if (typistUsers.length !== typistIds.length) {
throw new Error(`typist not found. ids: ${typistIds}`); throw new Error(`typist not found. ids: ${typistIds}`);
@ -1339,6 +1399,7 @@ export class TasksRepositoryService {
const typistGroups = await userGroupRepo.find({ const typistGroups = await userGroupRepo.find({
where: { account_id: accountId, id: In(groupIds) }, where: { account_id: accountId, id: In(groupIds) },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (typistGroups.length !== groupIds.length) { if (typistGroups.length !== groupIds.length) {
throw new Error(`typist group not found. ids: ${groupIds}`); throw new Error(`typist group not found. ids: ${groupIds}`);

View File

@ -52,6 +52,7 @@ export class TemplateFilesRepositoryService {
const template = await templateFilesRepo.findOne({ const template = await templateFilesRepo.findOne({
where: { account_id: accountId, file_name: fileName }, where: { account_id: accountId, file_name: fileName },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// 同名ファイルは同じものとして扱うため、すでにファイルがあれば更新(更新日時の履歴を残しておきたい) // 同名ファイルは同じものとして扱うため、すでにファイルがあれば更新(更新日時の履歴を残しておきたい)

View File

@ -12,3 +12,10 @@ export class TypistIdInvalidError extends Error {
this.name = 'TypistIdInvalidError'; this.name = 'TypistIdInvalidError';
} }
} }
// 同名のタイピストグループが存在する場合のエラー
export class TypistGroupNameAlreadyExistError extends Error {
constructor(message: string) {
super(message);
this.name = 'TypistGroupNameAlreadyExistError';
}
}

View File

@ -1,9 +1,13 @@
import { Injectable } from '@nestjs/common'; 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 { UserGroup } from './entity/user_group.entity';
import { UserGroupMember } from './entity/user_group_member.entity'; import { UserGroupMember } from './entity/user_group_member.entity';
import { User } from '../users/entity/user.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 { USER_ROLES } from '../../constants';
import { import {
insertEntities, insertEntities,
@ -122,6 +126,7 @@ export class UserGroupsRepositoryService {
role: USER_ROLES.TYPIST, role: USER_ROLES.TYPIST,
email_verified: true, email_verified: true,
}, },
lock: { mode: 'pessimistic_write' },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
}); });
if (userRecords.length !== typistIds.length) { 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に保存する // userGroupをDBに保存する
const userGroup = await insertEntity( const userGroup = await insertEntity(
UserGroup, UserGroup,
@ -188,6 +206,7 @@ export class UserGroupsRepositoryService {
role: USER_ROLES.TYPIST, role: USER_ROLES.TYPIST,
email_verified: true, email_verified: true,
}, },
lock: { mode: 'pessimistic_write' },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
}); });
if (userRecords.length !== typistIds.length) { 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が自アカウント内に存在するか確認する // GroupIdが自アカウント内に存在するか確認する
const typistGroup = await userGroupRepo.findOne({ const typistGroup = await userGroupRepo.findOne({
where: { where: {
id: typistGroupId, id: typistGroupId,
account_id: accountId, account_id: accountId,
}, },
lock: { mode: 'pessimistic_write' },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
}); });
if (!typistGroup) { if (!typistGroup) {

View File

@ -289,6 +289,7 @@ export class UsersRepositoryService {
const targetUser = await repo.findOne({ const targetUser = await repo.findOne({
where: { id: id, account_id: accountId }, where: { id: id, account_id: accountId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理 // 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理

View File

@ -87,6 +87,7 @@ export class WorkflowsRepositoryService {
const author = await userRepo.findOne({ const author = await userRepo.findOne({
where: { account_id: accountId, id: authorId, email_verified: true }, where: { account_id: accountId, id: authorId, email_verified: true },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!author) { if (!author) {
throw new UserNotFoundError( throw new UserNotFoundError(
@ -100,6 +101,7 @@ export class WorkflowsRepositoryService {
const worktypes = await worktypeRepo.find({ const worktypes = await worktypeRepo.find({
where: { account_id: accountId, id: worktypeId }, where: { account_id: accountId, id: worktypeId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (worktypes.length === 0) { if (worktypes.length === 0) {
throw new WorktypeIdNotFoundError( throw new WorktypeIdNotFoundError(
@ -114,6 +116,7 @@ export class WorkflowsRepositoryService {
const template = await templateRepo.findOne({ const template = await templateRepo.findOne({
where: { account_id: accountId, id: templateId }, where: { account_id: accountId, id: templateId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!template) { if (!template) {
throw new TemplateFileNotExistError('template not found.'); throw new TemplateFileNotExistError('template not found.');
@ -131,6 +134,7 @@ export class WorkflowsRepositoryService {
email_verified: true, email_verified: true,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (typistUsers.length !== typistIds.length) { if (typistUsers.length !== typistIds.length) {
throw new UserNotFoundError( throw new UserNotFoundError(
@ -146,6 +150,7 @@ export class WorkflowsRepositoryService {
const typistGroups = await userGroupRepo.find({ const typistGroups = await userGroupRepo.find({
where: { account_id: accountId, id: In(groupIds) }, where: { account_id: accountId, id: In(groupIds) },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (typistGroups.length !== groupIds.length) { if (typistGroups.length !== groupIds.length) {
throw new TypistGroupNotExistError( throw new TypistGroupNotExistError(
@ -163,6 +168,7 @@ export class WorkflowsRepositoryService {
worktype_id: worktypeId !== undefined ? worktypeId : IsNull(), worktype_id: worktypeId !== undefined ? worktypeId : IsNull(),
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (workflow.length !== 0) { if (workflow.length !== 0) {
throw new AuthorIdAndWorktypeIdPairAlreadyExistsError( throw new AuthorIdAndWorktypeIdPairAlreadyExistsError(
@ -227,23 +233,12 @@ export class WorkflowsRepositoryService {
): Promise<void> { ): Promise<void> {
return await this.dataSource.transaction(async (entityManager) => { return await this.dataSource.transaction(async (entityManager) => {
const workflowRepo = entityManager.getRepository(Workflow); 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の存在確認 // authorの存在確認
const userRepo = entityManager.getRepository(User); const userRepo = entityManager.getRepository(User);
const author = await userRepo.findOne({ const author = await userRepo.findOne({
where: { account_id: accountId, id: authorId, email_verified: true }, where: { account_id: accountId, id: authorId, email_verified: true },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!author) { if (!author) {
throw new UserNotFoundError( 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の存在確認 // worktypeの存在確認
if (worktypeId !== undefined) { if (worktypeId !== undefined) {
const worktypeRepo = entityManager.getRepository(Worktype); const worktypeRepo = entityManager.getRepository(Worktype);
const worktypes = await worktypeRepo.find({ const worktypes = await worktypeRepo.find({
where: { account_id: accountId, id: worktypeId }, where: { account_id: accountId, id: worktypeId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (worktypes.length === 0) { if (worktypes.length === 0) {
throw new WorktypeIdNotFoundError( throw new WorktypeIdNotFoundError(
@ -271,6 +298,7 @@ export class WorkflowsRepositoryService {
const template = await templateRepo.findOne({ const template = await templateRepo.findOne({
where: { account_id: accountId, id: templateId }, where: { account_id: accountId, id: templateId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!template) { if (!template) {
throw new TemplateFileNotExistError( 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) => { const groupIds = typists.flatMap((typist) => {
return typist.typistGroupId ? [typist.typistGroupId] : []; return typist.typistGroupId ? [typist.typistGroupId] : [];
@ -305,6 +315,7 @@ export class WorkflowsRepositoryService {
const typistGroups = await userGroupRepo.find({ const typistGroups = await userGroupRepo.find({
where: { account_id: accountId, id: In(groupIds) }, where: { account_id: accountId, id: In(groupIds) },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (typistGroups.length !== groupIds.length) { if (typistGroups.length !== groupIds.length) {
throw new TypistGroupNotExistError( throw new TypistGroupNotExistError(
@ -399,6 +410,7 @@ export class WorkflowsRepositoryService {
const workflow = await workflowRepo.findOne({ const workflow = await workflowRepo.findOne({
where: { account_id: accountId, id: workflowId }, where: { account_id: accountId, id: workflowId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!workflow) { if (!workflow) {
throw new WorkflowNotFoundError( throw new WorkflowNotFoundError(

View File

@ -88,6 +88,7 @@ export class WorktypesRepositoryService {
const duplicatedWorktype = await worktypeRepo.findOne({ const duplicatedWorktype = await worktypeRepo.findOne({
where: { account_id: accountId, custom_worktype_id: worktypeId }, where: { account_id: accountId, custom_worktype_id: worktypeId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// ワークタイプIDが重複している場合はエラー // ワークタイプIDが重複している場合はエラー
@ -100,6 +101,7 @@ export class WorktypesRepositoryService {
const worktypeCount = await worktypeRepo.count({ const worktypeCount = await worktypeRepo.count({
where: { account_id: accountId }, where: { account_id: accountId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// ワークタイプの登録数が上限に達している場合はエラー // ワークタイプの登録数が上限に達している場合はエラー
@ -163,6 +165,7 @@ export class WorktypesRepositoryService {
const worktype = await worktypeRepo.findOne({ const worktype = await worktypeRepo.findOne({
where: { account_id: accountId, id: id }, where: { account_id: accountId, id: id },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// ワークタイプが存在しない場合はエラー // ワークタイプが存在しない場合はエラー
@ -177,6 +180,7 @@ export class WorktypesRepositoryService {
id: Not(id), id: Not(id),
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// ワークタイプIDが重複している場合はエラー // ワークタイプIDが重複している場合はエラー
@ -216,6 +220,7 @@ export class WorktypesRepositoryService {
const worktype = await worktypeRepo.findOne({ const worktype = await worktypeRepo.findOne({
where: { account_id: accountId, id: id }, where: { account_id: accountId, id: id },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// ワークタイプが存在しない場合はエラー // ワークタイプが存在しない場合はエラー
if (!worktype) { if (!worktype) {
@ -227,6 +232,7 @@ export class WorktypesRepositoryService {
const account = await accountRepo.findOne({ const account = await accountRepo.findOne({
where: { id: accountId }, where: { id: accountId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (account?.active_worktype_id === id) { if (account?.active_worktype_id === id) {
@ -244,6 +250,7 @@ export class WorktypesRepositoryService {
const workflows = await workflowRepo.find({ const workflows = await workflowRepo.find({
where: { account_id: accountId, worktype_id: id }, where: { account_id: accountId, worktype_id: id },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (workflows.length > 0) { if (workflows.length > 0) {
const workflowIds = workflows.map((workflow) => workflow.id); const workflowIds = workflows.map((workflow) => workflow.id);
@ -322,6 +329,7 @@ export class WorktypesRepositoryService {
const worktype = await worktypeRepo.findOne({ const worktype = await worktypeRepo.findOne({
where: { account_id: accountId, id: worktypeId }, where: { account_id: accountId, id: worktypeId },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// ワークタイプが存在しない場合はエラー // ワークタイプが存在しない場合はエラー
if (!worktype) { if (!worktype) {