From 52b71a267bf15a63431c0b7c7e7b85393bc7732f Mon Sep 17 00:00:00 2001 From: masaaki Date: Wed, 24 Jan 2024 09:25:47 +0000 Subject: [PATCH 01/18] =?UTF-8?q?Merged=20PR=20695:=20=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3505: テスト対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3505) - アカウントIDの初期値を変更するためのSQLを作成 - 運用開始時に忘れないよう、登録を行うためのタスクを追加 タスク 3530: アカウントIDをユーザ指定の通番から開始するようにAUTO_INCRIMENTの値を変更する ## レビューポイント - 特にレビューしてほしい箇所 - SQLファイルの登録先。 既存のフォルダで意味として矛盾の無い「DB/init」配下としたが妥当か。 今後移行用のスクリプトの登録などが予想されるので、そこを見据えた方がよいか? ## UIの変更 - 無し ## 動作確認状況 - ローカルで確認 ## 補足 - レビューOKが出たタイミングで、dev/stg環境に一度適用を行います。 prod環境については、現在のデータを残す可能性があるので、適用は移行時に実施する想定です。  →(2024/1/24追記)データ移行のタイミングでデータは一度きれいにするので、prod環境にも適用する --- db/init/init_accounts_auto_increment.sql | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 db/init/init_accounts_auto_increment.sql diff --git a/db/init/init_accounts_auto_increment.sql b/db/init/init_accounts_auto_increment.sql new file mode 100644 index 0000000..7d0aa25 --- /dev/null +++ b/db/init/init_accounts_auto_increment.sql @@ -0,0 +1,4 @@ +-- [OMDS_IS-231] アカウントIDの開始番号調整 | 課題の表示 | Backlog 対応 +-- IDからアカウント数が推測されるため、ユーザ指定の任意値を最初の番号とする +-- 一度しか実行しないため、migrate fileではなくDBの初期値として扱う。移行時の実行を想定 +ALTER TABLE accounts AUTO_INCREMENT = 853211; From 7c83a9ccae1041f28ae5ae9cbeb4020aa0454d47 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Thu, 25 Jan 2024 06:53:02 +0000 Subject: [PATCH 02/18] =?UTF-8?q?Merged=20PR=20704:=20=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E8=AA=8D=E8=A8=BC=E3=83=A1=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E3=81=AEURL=E3=81=AE=E6=9C=9F=E9=99=90=E3=81=AB=E3=81=A4?= =?UTF-8?q?=E3=81=84=E3=81=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3515: ユーザー認証メールのURLの期限について](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3515) - 環境変数の有効期限に使用されている値を見直し - アクセストークンの有効期限 - ミリ秒→秒単位に修正 - リフレッシュトークンの有効期限 - ミリ秒→秒単位に修正 - 認証メールリンクの有効期限 - ミリ秒→秒単位に修正 - SASトークンの有効期限 - 有効期限が間違っていたので修正 - 2時間に修正(奥澤さんと調整済み) - キャッシュの有効期限 - 24時間であっていたのでそのまま ## レビューポイント - ほかに確認する値はあるか ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - トークン生成時に渡す有効期限情報が秒単位であっているのか値を変更して確認した ## 補足 - 相談、参考資料などがあれば --- dictation_server/.env.local.example | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dictation_server/.env.local.example b/dictation_server/.env.local.example index c59d570..8c03436 100644 --- a/dictation_server/.env.local.example +++ b/dictation_server/.env.local.example @@ -16,7 +16,7 @@ MAIL_FROM=xxxxx@xxxxx.xxxx NOTIFICATION_HUB_NAME=ntf-odms-dev NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX APP_DOMAIN=http://localhost:8081/ -STORAGE_TOKEN_EXPIRE_TIME=30 +STORAGE_TOKEN_EXPIRE_TIME=2 STORAGE_ACCOUNT_NAME_US=saodmsusdev STORAGE_ACCOUNT_NAME_AU=saodmsaudev STORAGE_ACCOUNT_NAME_EU=saodmseudev @@ -26,10 +26,10 @@ STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA -ACCESS_TOKEN_LIFETIME_WEB=7200000 -REFRESH_TOKEN_LIFETIME_WEB=86400000 -REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000 -EMAIL_CONFIRM_LIFETIME=86400000 +ACCESS_TOKEN_LIFETIME_WEB=7200 +REFRESH_TOKEN_LIFETIME_WEB=86400 +REFRESH_TOKEN_LIFETIME_DEFAULT=2592000 +EMAIL_CONFIRM_LIFETIME=86400 REDIS_HOST=redis-cache REDIS_PORT=6379 REDIS_PASSWORD=omdsredispass From 271d85482dd9d62fd1c16488cfa7bda2060dc2d3 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 26 Jan 2024 08:03:18 +0000 Subject: [PATCH 03/18] =?UTF-8?q?Merged=20PR=20705:=20=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3549: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3549) - 第五階層のユーザーがライセンス注文する際にディーラーが設定されていない場合に出るエラーを専用のメッセージとなるように修正しました。 ※文言はメッセージレビュー後に必要があれば修正します。 ## レビューポイント - エラーの仕分け方は適切でしょうか? ## UIの変更 - [Task3549](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3549?csf=1&web=1&e=uwyFMW) ## 動作確認状況 - ローカルで確認 --- .../src/features/license/licenseOrder/operations.ts | 6 ++++++ dictation_client/src/translation/de.json | 3 ++- dictation_client/src/translation/en.json | 3 ++- dictation_client/src/translation/es.json | 3 ++- dictation_client/src/translation/fr.json | 3 ++- dictation_server/src/features/licenses/licenses.service.ts | 5 ++++- 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/dictation_client/src/features/license/licenseOrder/operations.ts b/dictation_client/src/features/license/licenseOrder/operations.ts index aaf3a84..abd5981 100644 --- a/dictation_client/src/features/license/licenseOrder/operations.ts +++ b/dictation_client/src/features/license/licenseOrder/operations.ts @@ -62,6 +62,12 @@ export const orderLicenseAsync = createAsyncThunk< ); } + if (error.code === "E010501") { + errorMessage = getTranslationID( + "licenseOrderPage.message.dealerNotFoundError" + ); + } + thunkApi.dispatch( openSnackbar({ level: "error", diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 3f82e06..47716dd 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -189,7 +189,8 @@ "poNumberIncorrectError": "Das Format der Bestellnummer ist ungültig. Für die Bestellnummer können nur alphanumerische Zeichen eingegeben werden.", "newOrderIncorrectError": "Bitte geben Sie für die neue Bestellung eine Zahl größer oder gleich 1 ein.", "confirmOrder": "Möchten Sie eine Bestellung aufgeben?", - "poNumberConflictError": "Die eingegebene Bestellnummer existiert bereits. Bitte geben Sie eine andere Bestellnummer ein." + "poNumberConflictError": "Die eingegebene Bestellnummer existiert bereits. Bitte geben Sie eine andere Bestellnummer ein.", + "dealerNotFoundError": "(de)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。" }, "label": { "title": "Lizenz bestellen", diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 6b68ea1..ae656b3 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -190,7 +190,8 @@ "poNumberIncorrectError": "PO Number format is not valid. Only alphanumeric characters can be entered for the PO Number.", "newOrderIncorrectError": "Please enter a number greater than or equal to 1 for the New Order.", "confirmOrder": "Would you like to place an order?", - "poNumberConflictError": "PO Number entered already exists. Please enter a different PO Number." + "poNumberConflictError": "PO Number entered already exists. Please enter a different PO Number.", + "dealerNotFoundError": "ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。" }, "label": { "title": "Order License", diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 85cda56..bd228a5 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -190,7 +190,8 @@ "poNumberIncorrectError": "El formato del número de orden de compra no es válido. Sólo se pueden ingresar caracteres alfanuméricos para el número de orden de compra.", "newOrderIncorrectError": "Ingrese un número mayor o igual a 1 para el Nuevo Pedido.", "confirmOrder": "¿Quieres hacer un pedido?", - "poNumberConflictError": "El número de orden de compra ingresado ya existe. Ingrese un número de orden de compra diferente." + "poNumberConflictError": "El número de orden de compra ingresado ya existe. Ingrese un número de orden de compra diferente.", + "dealerNotFoundError": "(es)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。" }, "label": { "title": "Licencia de pedido", diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 036d2ea..f068d3d 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -190,7 +190,8 @@ "poNumberIncorrectError": "Le format du numéro de bon de commande n'est pas valide. Seuls des caractères alphanumériques peuvent être saisis pour le numéro de bon de commande.", "newOrderIncorrectError": "Veuillez saisir un nombre supérieur ou égal à 1 pour la nouvelle commande.", "confirmOrder": "Voulez-vous passer commande?", - "poNumberConflictError": "Le numéro de bon de commande saisi existe déjà. Veuillez saisir un autre numéro de bon de commande." + "poNumberConflictError": "Le numéro de bon de commande saisi existe déjà. Veuillez saisir un autre numéro de bon de commande.", + "dealerNotFoundError": "(fr)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。" }, "label": { "title": "Commander licence", diff --git a/dictation_server/src/features/licenses/licenses.service.ts b/dictation_server/src/features/licenses/licenses.service.ts index 5b5ef19..3f64729 100644 --- a/dictation_server/src/features/licenses/licenses.service.ts +++ b/dictation_server/src/features/licenses/licenses.service.ts @@ -80,7 +80,9 @@ export class LicensesService { .parent_account_id ?? undefined; // 親アカウントIDが取得できない場合はエラー if (parentAccountId === undefined) { - throw new Error('parent account id is undefined'); + throw new AccountNotFoundError( + `parent account id is not found. myAccountId: ${myAccountId}`, + ); } } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); @@ -147,6 +149,7 @@ export class LicensesService { ); } } + async issueCardLicenseKeys( context: Context, externalId: string, From 794dae0c15b54105e0ca0987734df21760e0d614 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Fri, 26 Jan 2024 08:58:08 +0000 Subject: [PATCH 04/18] =?UTF-8?q?Merged=20PR=20709:=20=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E5=AF=BE=E5=BF=9C=E3=82=82=E3=82=8C=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3589: テスト対応もれ修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3589) - バグ対応で漏れていたテスト修正を実施しました。 ## レビューポイント - 共有 ## UIの変更 -なし ## 動作確認状況 - ローカルで確認 --- .../src/features/licenses/licenses.service.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts index c3473ab..22be77f 100644 --- a/dictation_server/src/features/licenses/licenses.service.spec.ts +++ b/dictation_server/src/features/licenses/licenses.service.spec.ts @@ -192,8 +192,8 @@ describe('ライセンス注文', () => { await service.licenseOrders(context, externalId, poNumber, orderCount); } catch (e) { if (e instanceof HttpException) { - expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); - expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010501')); } else { fail(); } From 986b710aaad12d768f7202ef0fd6c00c3831fe2e Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Fri, 26 Jan 2024 11:07:56 +0000 Subject: [PATCH 05/18] =?UTF-8?q?Merged=20PR=20707:=20=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3507: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3507) アップロードとcheckout時にライセンスチェックを行う。ダウンロード時にはチェックを行わない。 - 影響範囲(他の機能にも影響があるか) checkoutのとき、第五階層の場合にチェックが行われる。 外部連携APIが返却するパラメータが変わるため、OMDSさんに連携する ## レビューポイント - 不要な個所まで削除していないか(アップロードの場合はチェックを残す) ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/constants/index.ts | 14 +- .../src/features/files/files.service.spec.ts | 450 ------------------ .../src/features/files/files.service.ts | 53 +-- .../src/features/tasks/tasks.module.ts | 2 + .../src/features/tasks/tasks.service.spec.ts | 215 ++++++++- .../src/features/tasks/tasks.service.ts | 44 +- .../features/tasks/test/tasks.service.mock.ts | 8 + .../src/features/users/types/types.ts | 6 +- .../src/features/users/users.service.spec.ts | 14 +- .../src/features/users/users.service.ts | 10 +- .../licenses/licenses.repository.service.ts | 16 +- 11 files changed, 307 insertions(+), 525 deletions(-) diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index 511e4b8..6657210 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -219,9 +219,9 @@ export const PNS = { }; /** - * ユーザーのライセンス状態 + * ユーザーのライセンスの有効期限の状態 */ -export const USER_LICENSE_STATUS = { +export const USER_LICENSE_EXPIRY_STATUS = { NORMAL: 'Normal', NO_LICENSE: 'NoLicense', ALERT: 'Alert', @@ -311,3 +311,13 @@ export const USER_AUDIO_FORMAT = 'DS2(QP)'; * @const {string[]} */ export const NODE_ENV_TEST = 'test'; + +/** + * ユーザに対するライセンスの状態 + * @const {string[]} + */ +export const USER_LICENSE_STATUS = { + UNALLOCATED: 'unallocated', + ALLOCATED: 'allocated', + EXPIRED: 'expired', +} as const; diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index 17d1253..deff777 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -235,67 +235,6 @@ describe('publishUploadSas', () => { new HttpException(makeErrorResponse('E010812'), HttpStatus.BAD_REQUEST), ); }); - it('アップロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { - external_id: externalId, - id: userId, - author_id: authorId, - } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); - // 昨日の日付を作成 - let yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - yesterday = new DateWithZeroTime(yesterday); - // 期限切れのライセンスを作成して紐づける - await createLicense( - source, - 1, - yesterday, - tier5Accounts.account.id, - LICENSE_TYPE.NORMAL, - LICENSE_ALLOCATED_STATUS.ALLOCATED, - userId, - null, - null, - null, - ); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishUploadSas = `${url}?sas-token`; - blobParam.fileExists = false; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - await expect( - service.publishUploadSas( - makeContext('trackingId', 'requestId'), - externalId, - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST), - ); - }); }); describe('タスク作成から自動ルーティング(DB使用)', () => { @@ -1097,76 +1036,6 @@ describe('音声ファイルダウンロードURL取得', () => { ), ).toEqual(`${url}?sas-token`); }); - it('ダウンロードSASトークンが乗っているURLを取得できる(第五階層の場合ライセンスのチェックを行う)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { - external_id: externalId, - id: userId, - author_id: authorId, - } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); - // 本日の日付を作成 - let today = new Date(); - today.setDate(today.getDate()); - today = new DateWithZeroTime(today); - // 有効期限内のライセンスを作成して紐づける - await createLicense( - source, - 1, - today, - tier5Accounts.account.id, - LICENSE_TYPE.NORMAL, - LICENSE_ALLOCATED_STATUS.ALLOCATED, - userId, - null, - null, - null, - ); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - - const { audioFileId } = await createTask( - source, - tier5Accounts.account.id, - url, - 'test.zip', - 'InProgress', - undefined, - authorId ?? '', - ); - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishDownloadSas = `${url}?sas-token`; - blobParam.fileExists = true; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - expect( - await service.publishAudioFileDownloadSas( - makeContext('trackingId', 'requestId'), - externalId, - audioFileId, - ), - ).toEqual(`${url}?sas-token`); - }); it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); @@ -1396,133 +1265,6 @@ describe('音声ファイルダウンロードURL取得', () => { new HttpException(makeErrorResponse('E010701'), HttpStatus.BAD_REQUEST), ); }); - it('ダウンロード時にユーザーにライセンスが未割当の場合エラーとなる(第五階層限定)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する(ライセンスは作成しない) - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { - external_id: externalId, - id: userId, - author_id: authorId, - } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - - const { audioFileId } = await createTask( - source, - tier5Accounts.account.id, - url, - 'test.zip', - 'InProgress', - undefined, - authorId ?? '', - ); - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishDownloadSas = `${url}?sas-token`; - blobParam.fileExists = false; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - await expect( - service.publishAudioFileDownloadSas( - makeContext('trackingId', 'requestId'), - externalId, - audioFileId, - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010812'), HttpStatus.BAD_REQUEST), - ); - }); - it('ダウンロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { - external_id: externalId, - id: userId, - author_id: authorId, - } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); - // 昨日の日付を作成 - let yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - yesterday = new DateWithZeroTime(yesterday); - // 期限切れのライセンスを作成して紐づける - await createLicense( - source, - 1, - yesterday, - tier5Accounts.account.id, - LICENSE_TYPE.NORMAL, - LICENSE_ALLOCATED_STATUS.ALLOCATED, - userId, - null, - null, - null, - ); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - - const { audioFileId } = await createTask( - source, - tier5Accounts.account.id, - url, - 'test.zip', - 'InProgress', - undefined, - authorId ?? '', - ); - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishDownloadSas = `${url}?sas-token`; - blobParam.fileExists = false; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - await expect( - service.publishAudioFileDownloadSas( - makeContext('trackingId', 'requestId'), - externalId, - audioFileId, - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST), - ); - }); }); describe('テンプレートファイルダウンロードURL取得', () => { @@ -1596,70 +1338,7 @@ describe('テンプレートファイルダウンロードURL取得', () => { ); expect(resultUrl).toBe(`${url}?sas-token`); }); - it('ダウンロードSASトークンが乗っているURLを取得できる(第五階層の場合ライセンスのチェックを行う)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { external_id: externalId, id: userId } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'typist-user-external-id', - role: USER_ROLES.TYPIST, - }); - // 本日の日付を作成 - let yesterday = new Date(); - yesterday.setDate(yesterday.getDate()); - yesterday = new DateWithZeroTime(yesterday); - // 有効期限内のライセンスを作成して紐づける - await createLicense( - source, - 1, - yesterday, - tier5Accounts.account.id, - LICENSE_TYPE.NORMAL, - LICENSE_ALLOCATED_STATUS.ALLOCATED, - userId, - null, - null, - null, - ); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - const { audioFileId } = await createTask( - source, - tier5Accounts.account.id, - url, - 'test.zip', - TASK_STATUS.IN_PROGRESS, - userId, - 'AUTHOR_ID', - ); - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishDownloadSas = `${url}?sas-token`; - blobParam.fileExists = true; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - const resultUrl = await service.publishTemplateFileDownloadSas( - makeContext('trackingId', 'requestId'), - externalId, - audioFileId, - ); - expect(resultUrl).toBe(`${url}?sas-token`); - }); it('タスクのステータスが[Inprogress,Pending]以外でエラー', async () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); @@ -1849,135 +1528,6 @@ describe('テンプレートファイルダウンロードURL取得', () => { } } }); - it('ダウンロード時にユーザーにライセンスが未割当の場合エラーとなる(第五階層限定)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する(ライセンスは作成しない) - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { external_id: externalId, id: userId } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'typist-user-external-id', - role: USER_ROLES.TYPIST, - }); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - - const { audioFileId } = await createTask( - source, - tier5Accounts.account.id, - url, - 'test.zip', - TASK_STATUS.IN_PROGRESS, - undefined, - 'AUTHOR_ID', - ); - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishDownloadSas = `${url}?sas-token`; - blobParam.fileExists = false; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - try { - await service.publishTemplateFileDownloadSas( - makeContext('trackingId', 'requestId'), - externalId, - audioFileId, - ); - fail(); - } catch (e) { - if (e instanceof HttpException) { - expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); - expect(e.getResponse()).toEqual(makeErrorResponse('E010812')); - } else { - fail(); - } - } - }); - it('ダウンロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { external_id: externalId, id: userId } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'typist-user-external-id', - role: USER_ROLES.TYPIST, - }); - // 昨日の日付を作成 - let yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - yesterday = new DateWithZeroTime(yesterday); - // 期限切れのライセンスを作成して紐づける - await createLicense( - source, - 1, - yesterday, - tier5Accounts.account.id, - LICENSE_TYPE.NORMAL, - LICENSE_ALLOCATED_STATUS.ALLOCATED, - userId, - null, - null, - null, - ); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - - const { audioFileId } = await createTask( - source, - tier5Accounts.account.id, - url, - 'test.zip', - TASK_STATUS.IN_PROGRESS, - undefined, - 'AUTHOR_ID', - ); - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishDownloadSas = `${url}?sas-token`; - blobParam.fileExists = false; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - try { - await service.publishTemplateFileDownloadSas( - makeContext('trackingId', 'requestId'), - externalId, - audioFileId, - ), - fail(); - } catch (e) { - if (e instanceof HttpException) { - expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); - expect(e.getResponse()).toEqual(makeErrorResponse('E010805')); - } else { - fail(); - } - } - }); }); describe('publishTemplateFileUploadSas', () => { diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts index 6c1b558..1f9c042 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -8,6 +8,7 @@ import { OPTION_ITEM_NUM, TASK_STATUS, TIERS, + USER_LICENSE_STATUS, USER_ROLES, } from '../../constants/index'; import { User } from '../../repositories/users/entity/user.entity'; @@ -308,10 +309,10 @@ export class FilesService { context, user.id, ); - if (state === 'expired') { + if (state === USER_LICENSE_STATUS.EXPIRED) { throw new LicenseExpiredError('license is expired.'); } - if (state === 'inallocated') { + if (state === USER_LICENSE_STATUS.UNALLOCATED) { throw new LicenseNotAllocatedError('license is not allocated.'); } } @@ -392,20 +393,6 @@ export class FilesService { if (!user.account) { throw new AccountNotFoundError('account not found.'); } - // 第五階層のみチェック - if (user.account.tier === TIERS.TIER5) { - // ライセンスが有効でない場合、エラー - const { state } = await this.licensesRepository.getLicenseState( - context, - user.id, - ); - if (state === 'expired') { - throw new LicenseExpiredError('license is expired.'); - } - if (state === 'inallocated') { - throw new LicenseNotAllocatedError('license is not allocated.'); - } - } accountId = user.account.id; userId = user.id; country = user.account.country; @@ -422,16 +409,6 @@ export class FilesService { }`, ); switch (e.constructor) { - case LicenseExpiredError: - throw new HttpException( - makeErrorResponse('E010805'), - HttpStatus.BAD_REQUEST, - ); - case LicenseNotAllocatedError: - throw new HttpException( - makeErrorResponse('E010812'), - HttpStatus.BAD_REQUEST, - ); default: throw new HttpException( makeErrorResponse('E009999'), @@ -571,20 +548,6 @@ export class FilesService { if (!user.account) { throw new AccountNotFoundError('account not found.'); } - // 第五階層のみチェック - if (user.account.tier === TIERS.TIER5) { - // ライセンスが有効でない場合、エラー - const { state } = await this.licensesRepository.getLicenseState( - context, - user.id, - ); - if (state === 'expired') { - throw new LicenseExpiredError('license is expired.'); - } - if (state === 'inallocated') { - throw new LicenseNotAllocatedError('license is not allocated.'); - } - } accountId = user.account_id; userId = user.id; country = user.account.country; @@ -596,16 +559,6 @@ export class FilesService { }`, ); switch (e.constructor) { - case LicenseExpiredError: - throw new HttpException( - makeErrorResponse('E010805'), - HttpStatus.BAD_REQUEST, - ); - case LicenseNotAllocatedError: - throw new HttpException( - makeErrorResponse('E010812'), - HttpStatus.BAD_REQUEST, - ); default: throw new HttpException( makeErrorResponse('E009999'), diff --git a/dictation_server/src/features/tasks/tasks.module.ts b/dictation_server/src/features/tasks/tasks.module.ts index c3e10e0..b95229b 100644 --- a/dictation_server/src/features/tasks/tasks.module.ts +++ b/dictation_server/src/features/tasks/tasks.module.ts @@ -8,6 +8,7 @@ import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_ import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module'; import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module'; +import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module'; @Module({ imports: [ @@ -18,6 +19,7 @@ import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.r AdB2cModule, NotificationhubModule, SendGridModule, + LicensesRepositoryModule, ], providers: [TasksService], controllers: [TasksController], diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 712f498..d50233a 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -25,7 +25,13 @@ import { makeTestSimpleAccount, makeTestUser, } from '../../common/test/utility'; -import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants'; +import { + ADMIN_ROLES, + LICENSE_ALLOCATED_STATUS, + LICENSE_TYPE, + TASK_STATUS, + USER_ROLES, +} from '../../constants'; import { makeTestingModule } from '../../common/test/modules'; import { createSortCriteria } from '../users/test/utility'; import { createWorktype } from '../accounts/test/utility'; @@ -38,6 +44,9 @@ import { NotificationhubService } from '../../gateways/notificationhub/notificat import { Roles } from '../../common/types/role'; import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; import { truncateAllTable } from '../../common/test/init'; +import { makeDefaultLicensesRepositoryMockValue } from '../accounts/test/accounts.service.mock'; +import { DateWithZeroTime } from '../licenses/types/types'; +import { createLicense } from '../licenses/test/utility'; describe('TasksService', () => { it('タスク一覧を取得できる(admin)', async () => { @@ -48,12 +57,15 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); const service = await makeTasksServiceMock( tasksRepositoryMockValue, usersRepositoryMockValue, userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -122,6 +134,8 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); usersRepositoryMockValue.findUserByExternalId = new Error('DB failed'); const service = await makeTasksServiceMock( tasksRepositoryMockValue, @@ -129,6 +143,7 @@ describe('TasksService', () => { userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -164,6 +179,8 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); tasksRepositoryMockValue.getTasksFromAccountId = new Error('DB failed'); const service = await makeTasksServiceMock( tasksRepositoryMockValue, @@ -171,6 +188,7 @@ describe('TasksService', () => { userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -252,12 +270,15 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); const service = await makeTasksServiceMock( tasksRepositoryMockValue, usersRepositoryMockValue, userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; const offset = 0; @@ -292,6 +313,8 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); if (usersRepositoryMockValue.findUserByExternalId instanceof Error) { return; } @@ -302,6 +325,7 @@ describe('TasksService', () => { userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -376,6 +400,8 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); tasksRepositoryMockValue.getTasksFromAuthorIdAndAccountId = new Error( 'DB failed', ); @@ -385,6 +411,7 @@ describe('TasksService', () => { userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -420,6 +447,8 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); if (usersRepositoryMockValue.findUserByExternalId instanceof Error) { return; } @@ -431,6 +460,7 @@ describe('TasksService', () => { userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -508,12 +538,15 @@ describe('TasksService', () => { tasksRepositoryMockValue.getTasksFromTypistRelations = new Error( 'DB failed', ); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); const service = await makeTasksServiceMock( tasksRepositoryMockValue, usersRepositoryMockValue, userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -549,6 +582,8 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); adb2cServiceMockValue.getUsers = new Adb2cTooManyRequestsError(); const service = await makeTasksServiceMock( tasksRepositoryMockValue, @@ -556,6 +591,7 @@ describe('TasksService', () => { userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -1632,7 +1668,75 @@ describe('checkout', () => { user_group_id: null, }); }); + it('第五階層のアカウントの場合、有効なライセンスが割当されている場合チェックアウトできる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウントを作成 + const { id: accountId } = await makeTestSimpleAccount(source, { tier: 5 }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id', + role: 'typist', + }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'MY_AUTHOR_ID', + }); + // 本日の日付を作成 + const today = new Date(); + // 有効なライセンスを作成して紐づける + await createLicense( + source, + 1, + today, + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + typistUserId, + null, + null, + null, + ); + const { taskId } = await createTask( + source, + accountId, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000001', + 'Pending', + ); + await createCheckoutPermissions(source, taskId, typistUserId); + const service = module.get(TasksService); + + const initTask = await getTask(source, taskId); + + await service.checkout( + makeContext('trackingId', 'requestId'), + 1, + ['typist'], + 'typist-user-external-id', + ); + const resultTask = await getTask(source, taskId); + const permisions = await getCheckoutPermissions(source, taskId); + + expect(resultTask?.status).toEqual('InProgress'); + expect(resultTask?.typist_user_id).toEqual(typistUserId); + //タスクの元々のステータスがPending,Inprogressの場合、文字起こし開始時刻は更新されない + expect(resultTask?.started_at).toEqual(initTask?.started_at); + expect(permisions.length).toEqual(1); + expect(permisions[0]).toEqual({ + id: 2, + task_id: 1, + user_id: 1, + user_group_id: null, + }); + }); it('ユーザーのRoleがTypistで、対象のタスクのStatus[Uploaded,Inprogress,Pending]以外の時、タスクをチェックアウトできない', async () => { if (!source) fail(); const module = await makeTestingModule(source); @@ -1678,7 +1782,116 @@ describe('checkout', () => { } } }); + it('第五階層のアカウントの場合、ライセンスが未割当の場合チェックアウトできない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウントを作成 + const { id: accountId } = await makeTestSimpleAccount(source, { tier: 5 }); + await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id', + role: 'typist', + }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'MY_AUTHOR_ID', + }); + await createTask( + source, + accountId, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000001', + 'Backup', + ); + const service = module.get(TasksService); + try { + await service.checkout( + makeContext('trackingId', 'requestId'), + 1, + ['typist'], + 'typist-user-external-id', + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010812')); + } else { + fail(); + } + } + }); + it('第五階層のアカウントの場合、ライセンスが有効期限切れの場合チェックアウトできない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウントを作成 + const { id: accountId } = await makeTestSimpleAccount(source, { tier: 5 }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id', + role: 'typist', + }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'MY_AUTHOR_ID', + }); + // 昨日の日付を作成 + let yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday = new DateWithZeroTime(yesterday); + // 期限切れのライセンスを作成して紐づける + await createLicense( + source, + 1, + yesterday, + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + typistUserId, + null, + null, + null, + ); + + await createTask( + source, + accountId, + authorUserId, + 'MY_AUTHOR_ID', + '', + '01', + '00000001', + 'Backup', + ); + + const service = module.get(TasksService); + try { + await service.checkout( + makeContext('trackingId', 'requestId'), + 1, + ['typist'], + 'typist-user-external-id', + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010805')); + } else { + fail(); + } + } + }); it('ユーザーのRoleがTypistで、チェックアウト権限が存在しない時、タスクをチェックアウトできない', async () => { if (!source) fail(); const module = await makeTestingModule(source); diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts index d56055b..7b7c127 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -9,7 +9,13 @@ import { SortDirection, TaskListSortableAttribute, } from '../../common/types/sort'; -import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants'; +import { + ADMIN_ROLES, + TASK_STATUS, + TIERS, + USER_LICENSE_STATUS, + USER_ROLES, +} from '../../constants'; import { AdB2cService, Adb2cTooManyRequestsError, @@ -36,6 +42,12 @@ import { User } from '../../repositories/users/entity/user.entity'; import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; +import { AccountNotFoundError } from '../../repositories/accounts/errors/types'; +import { + LicenseExpiredError, + LicenseNotAllocatedError, +} from '../../repositories/licenses/errors/types'; +import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; @Injectable() export class TasksService { @@ -48,6 +60,7 @@ export class TasksService { private readonly adB2cService: AdB2cService, private readonly sendgridService: SendGridService, private readonly notificationhubService: NotificationhubService, + private readonly licensesRepository: LicensesRepositoryService, ) {} async getTasks( @@ -276,9 +289,26 @@ export class TasksService { } | params: { audioFileId: ${audioFileId}, roles: ${roles}, externalId: ${externalId} };`, ); - const { id, account_id, author_id } = + const { id, account_id, author_id, account } = await this.usersRepository.findUserByExternalId(context, externalId); + if (!account) { + throw new AccountNotFoundError('account not found.'); + } + // 第五階層のみチェック + if (account.tier === TIERS.TIER5) { + // ライセンスが有効でない場合、エラー + const { state } = await this.licensesRepository.getLicenseState( + context, + id, + ); + if (state === USER_LICENSE_STATUS.EXPIRED) { + throw new LicenseExpiredError('license is expired.'); + } + if (state === USER_LICENSE_STATUS.UNALLOCATED) { + throw new LicenseNotAllocatedError('license is not allocated.'); + } + } if (roles.includes(USER_ROLES.AUTHOR)) { // API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする if (!author_id) { @@ -308,6 +338,16 @@ export class TasksService { this.logger.error(`[${context.getTrackingId()}] error=${e}`); if (e instanceof Error) { switch (e.constructor) { + case LicenseExpiredError: + throw new HttpException( + makeErrorResponse('E010805'), + HttpStatus.BAD_REQUEST, + ); + case LicenseNotAllocatedError: + throw new HttpException( + makeErrorResponse('E010812'), + HttpStatus.BAD_REQUEST, + ); case CheckoutPermissionNotFoundError: case TaskAuthorIdNotMatchError: case InvalidRoleError: diff --git a/dictation_server/src/features/tasks/test/tasks.service.mock.ts b/dictation_server/src/features/tasks/test/tasks.service.mock.ts index 70dc66a..024f763 100644 --- a/dictation_server/src/features/tasks/test/tasks.service.mock.ts +++ b/dictation_server/src/features/tasks/test/tasks.service.mock.ts @@ -17,6 +17,11 @@ import { NotificationhubService } from '../../../gateways/notificationhub/notifi import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service'; import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service'; import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service'; +import { + LicensesRepositoryMockValue, + makeLicensesRepositoryMock, +} from '../../accounts/test/accounts.service.mock'; +import { LicensesRepositoryService } from '../../../repositories/licenses/licenses.repository.service'; export type TasksRepositoryMockValue = { getTasksFromAccountId: @@ -65,6 +70,7 @@ export const makeTasksServiceMock = async ( userGroupsRepositoryMockValue: UserGroupsRepositoryMockValue, adB2CServiceMockValue: AdB2CServiceMockValue, notificationhubServiceMockValue: NotificationhubServiceMockValue, + licensesRepositoryMockValue: LicensesRepositoryMockValue, ): Promise<{ tasksService: TasksService; taskRepoService: TasksRepositoryService; @@ -92,6 +98,8 @@ export const makeTasksServiceMock = async ( // メール送信でしか利用しておらず、テストする必要がないが、依存関係解決のため空オブジェクトを定義しておく。 case SendGridService: return {}; + case LicensesRepositoryService: + return makeLicensesRepositoryMock(licensesRepositoryMockValue); } }) .compile(); diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index 80b1a13..7cd4e44 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -9,7 +9,7 @@ import { } from 'class-validator'; import { TASK_LIST_SORTABLE_ATTRIBUTES, - USER_LICENSE_STATUS, + USER_LICENSE_EXPIRY_STATUS, } from '../../../constants'; import { USER_ROLES } from '../../../constants'; import { @@ -67,9 +67,9 @@ export class User { remaining?: number; @ApiProperty({ - description: `${Object.values(USER_LICENSE_STATUS).join('/')}`, + description: `${Object.values(USER_LICENSE_EXPIRY_STATUS).join('/')}`, }) - @IsIn(Object.values(USER_LICENSE_STATUS), { + @IsIn(Object.values(USER_LICENSE_EXPIRY_STATUS), { message: 'invalid license status', }) licenseStatus: string; diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 8e4a9fe..cb27055 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -22,7 +22,7 @@ import { LICENSE_EXPIRATION_THRESHOLD_DAYS, LICENSE_TYPE, USER_AUDIO_FORMAT, - USER_LICENSE_STATUS, + USER_LICENSE_EXPIRY_STATUS, USER_ROLES, } from '../../constants'; import { makeTestingModule } from '../../common/test/modules'; @@ -1479,7 +1479,7 @@ describe('UsersService.getUsers', () => { prompt: false, expiration: undefined, remaining: undefined, - licenseStatus: USER_LICENSE_STATUS.NO_LICENSE, + licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE, }, { id: typistUserId, @@ -1495,7 +1495,7 @@ describe('UsersService.getUsers', () => { prompt: false, expiration: undefined, remaining: undefined, - licenseStatus: USER_LICENSE_STATUS.NO_LICENSE, + licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE, }, { id: noneUserId, @@ -1511,7 +1511,7 @@ describe('UsersService.getUsers', () => { prompt: false, expiration: undefined, remaining: undefined, - licenseStatus: USER_LICENSE_STATUS.NO_LICENSE, + licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE, }, ]; @@ -1591,7 +1591,7 @@ describe('UsersService.getUsers', () => { date1.getMonth() + 1 }/${date1.getDate()}`, remaining: LICENSE_EXPIRATION_THRESHOLD_DAYS + 1, - licenseStatus: USER_LICENSE_STATUS.NORMAL, + licenseStatus: USER_LICENSE_EXPIRY_STATUS.NORMAL, }, { id: user2, @@ -1609,7 +1609,7 @@ describe('UsersService.getUsers', () => { date2.getMonth() + 1 }/${date2.getDate()}`, remaining: LICENSE_EXPIRATION_THRESHOLD_DAYS, - licenseStatus: USER_LICENSE_STATUS.RENEW, + licenseStatus: USER_LICENSE_EXPIRY_STATUS.RENEW, }, { id: user3, @@ -1627,7 +1627,7 @@ describe('UsersService.getUsers', () => { date3.getMonth() + 1 }/${date3.getDate()}`, remaining: LICENSE_EXPIRATION_THRESHOLD_DAYS - 1, - licenseStatus: USER_LICENSE_STATUS.ALERT, + licenseStatus: USER_LICENSE_EXPIRY_STATUS.ALERT, }, ]; diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index e48e24f..359f163 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -37,7 +37,7 @@ import { MANUAL_RECOVERY_REQUIRED, OPTION_ITEM_VALUE_TYPE_NUMBER, USER_AUDIO_FORMAT, - USER_LICENSE_STATUS, + USER_LICENSE_EXPIRY_STATUS, USER_ROLES, } from '../../constants'; import { DateWithZeroTime } from '../licenses/types/types'; @@ -617,7 +617,7 @@ export class UsersService { throw new Error('mail not found.'); } - let status = USER_LICENSE_STATUS.NORMAL; + let status = USER_LICENSE_EXPIRY_STATUS.NORMAL; // ライセンスの有効期限と残日数は、ライセンスが存在する場合のみ算出する // ライセンスが存在しない場合は、undefinedのままとする @@ -648,11 +648,11 @@ export class UsersService { remaining <= LICENSE_EXPIRATION_THRESHOLD_DAYS ) { status = dbUser.auto_renew - ? USER_LICENSE_STATUS.RENEW - : USER_LICENSE_STATUS.ALERT; + ? USER_LICENSE_EXPIRY_STATUS.RENEW + : USER_LICENSE_EXPIRY_STATUS.ALERT; } } else { - status = USER_LICENSE_STATUS.NO_LICENSE; + status = USER_LICENSE_EXPIRY_STATUS.NO_LICENSE; } return { diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 9f9cb85..5aeb576 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -15,6 +15,7 @@ import { NODE_ENV_TEST, SWITCH_FROM_TYPE, TIERS, + USER_LICENSE_STATUS, } from '../../constants'; import { PoNumberAlreadyExistError, @@ -806,12 +807,17 @@ export class LicensesRepositoryService { * ライセンスの割当状態を取得します * @param userId ユーザーID * @error { Error } DBアクセス失敗時の例外 - * @returns Promise<{ state: 'allocated' | 'inallocated' | 'expired' }> + * @returns Promise<{ state: 'allocated' | 'unallocated' | 'expired' }> */ async getLicenseState( context: Context, userId: number, - ): Promise<{ state: 'allocated' | 'inallocated' | 'expired' }> { + ): Promise<{ + state: + | typeof USER_LICENSE_STATUS.ALLOCATED + | typeof USER_LICENSE_STATUS.UNALLOCATED + | typeof USER_LICENSE_STATUS.EXPIRED; + }> { const allocatedLicense = await this.dataSource .getRepository(License) .findOne({ @@ -824,7 +830,7 @@ export class LicensesRepositoryService { // ライセンスが割り当てられていない場合は未割当状態 if (allocatedLicense == null) { - return { state: 'inallocated' }; + return { state: USER_LICENSE_STATUS.UNALLOCATED }; } // ライセンスの有効期限が過ぎている場合は期限切れ状態 @@ -833,9 +839,9 @@ export class LicensesRepositoryService { allocatedLicense.expiry_date && allocatedLicense.expiry_date < currentDate ) { - return { state: 'expired' }; + return { state: USER_LICENSE_STATUS.EXPIRED }; } - return { state: 'allocated' }; + return { state: USER_LICENSE_STATUS.ALLOCATED }; } } From 4d462a883a37ac0504c41f8cfef2cdf2e9662f83 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 29 Jan 2024 02:52:24 +0000 Subject: [PATCH 06/18] =?UTF-8?q?Merged=20PR=20688:=20=E8=A1=8C=E3=83=AD?= =?UTF-8?q?=E3=83=83=E3=82=AF=E6=A8=AA=E5=B1=95=E9=96=8B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3468: 行ロック横展開1](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3468) - 各リポジトリのメソッドについて行ロックが必要な箇所に処理を追加しました。 - **accounts** - **cancelIssue** - `canselIssue`と`allocateLicense`での対象ライセンス取得にロックを追加しました。 - **updateAccountInfo** - `updateAccountInfo`でのプライマリ/セカンダリ管理者取得にロックを追加しました。 - **templates** - **upsertTemplateFile** - `upsertTemplateFile`のテンプレートファイル取得にロックを追加しました。 - **users** - **update** - ユーザー取得にロックを追加しました。 - 影響としてはAuthorIDの重複が考えられたのでその対応のために入れています。 - **findDelegateUser** - selectのみでデータの不整合はないので特に処置はしていません。 - **isAllowDelegationPermission** - selectのみでデータの不整合はないので特に処置はしていません。 ※こちらの資料を参考に各メソッド内で影響に関連すると思われるselectにロックを追加しています。 [行ロックに関する影響調査.xlsx](https://ndstokyo.sharepoint.com/:x:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E8%A1%8C%E3%83%AD%E3%83%83%E3%82%AF%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E5%BD%B1%E9%9F%BF%E8%AA%BF%E6%9F%BB.xlsx?d=wdd6f3d97f7b04a538095c459f8eee2eb&csf=1&web=1&e=saqcTC) 上記資料を参考にタスク内で担当するメソッドについてロックの対応箇所を整理しました。 [Task3520](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3520?csf=1&web=1&e=ewuJoe) ## レビューポイント - 競合の対応として適切でしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルでマイグレーション確認 --- ...053-add_accounts_users_templates_index.sql | 19 +++++++++++++++++++ .../accounts/accounts.repository.service.ts | 3 +++ .../licenses/licenses.repository.service.ts | 7 ++----- .../template_files.repository.service.ts | 1 + .../users/users.repository.service.ts | 1 + 5 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 dictation_server/db/migrations/053-add_accounts_users_templates_index.sql diff --git a/dictation_server/db/migrations/053-add_accounts_users_templates_index.sql b/dictation_server/db/migrations/053-add_accounts_users_templates_index.sql new file mode 100644 index 0000000..5b4a4a6 --- /dev/null +++ b/dictation_server/db/migrations/053-add_accounts_users_templates_index.sql @@ -0,0 +1,19 @@ +-- +migrate Up +ALTER TABLE `accounts` ADD INDEX `idx_accounts_tier` (tier); +ALTER TABLE `accounts` ADD INDEX `idx_accounts_parent_account_id` (parent_account_id); +ALTER TABLE `users` ADD INDEX `idx_users_external_id` (external_id); +ALTER TABLE `users` ADD INDEX `idx_users_email_verified` (email_verified); +ALTER TABLE `licenses` ADD INDEX `idx_licenses_order_id` (order_id); +ALTER TABLE `licenses` ADD INDEX `idx_licenses_status` (status); +ALTER TABLE `template_files` ADD INDEX `idx_template_files_account_id` (account_id); +ALTER TABLE `template_files` ADD INDEX `idx_template_files_file_name` (file_name(500)); + +-- +migrate Down +ALTER TABLE `accounts` DROP INDEX `idx_accounts_tier`; +ALTER TABLE `accounts` DROP INDEX `idx_accounts_parent_account_id`; +ALTER TABLE `users` DROP INDEX `idx_users_external_id`; +ALTER TABLE `users` DROP INDEX `idx_users_email_verified`; +ALTER TABLE `licenses` DROP INDEX `idx_licenses_order_id`; +ALTER TABLE `licenses` DROP INDEX `idx_licenses_status`; +ALTER TABLE `template_files` DROP INDEX `idx_template_files_account_id`; +ALTER TABLE `template_files` DROP INDEX `idx_template_files_file_name`; \ No newline at end of file diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index 6d7c758..2832672 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -821,6 +821,7 @@ export class AccountsRepositoryService { status: Not(LICENSE_ALLOCATED_STATUS.UNALLOCATED), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // 存在した場合エラー @@ -1023,6 +1024,7 @@ export class AccountsRepositoryService { email_verified: true, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!primaryAdminUser) { throw new AdminUserNotFoundError( @@ -1040,6 +1042,7 @@ export class AccountsRepositoryService { email_verified: true, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!secondryAdminUser) { throw new AdminUserNotFoundError( diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 5aeb576..a5e03a3 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -12,7 +12,6 @@ import { LICENSE_ALLOCATED_STATUS, LICENSE_ISSUE_STATUS, LICENSE_TYPE, - NODE_ENV_TEST, SWITCH_FROM_TYPE, TIERS, USER_LICENSE_STATUS, @@ -423,10 +422,7 @@ export class LicensesRepositoryService { po_number: poNumber, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, - // テスト環境の場合はロックを行わない(sqliteがlockに対応していないため) - ...(process.env.NODE_ENV !== NODE_ENV_TEST - ? { lock: { mode: 'pessimistic_write' } } - : {}), + lock: { mode: 'pessimistic_write' }, }); if (!issuingOrder) { // 注文が存在しない場合、エラー @@ -570,6 +566,7 @@ export class LicensesRepositoryService { id: newLicenseId, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // ライセンスが存在しない場合はエラー diff --git a/dictation_server/src/repositories/template_files/template_files.repository.service.ts b/dictation_server/src/repositories/template_files/template_files.repository.service.ts index e3d3319..5fc805b 100644 --- a/dictation_server/src/repositories/template_files/template_files.repository.service.ts +++ b/dictation_server/src/repositories/template_files/template_files.repository.service.ts @@ -52,6 +52,7 @@ export class TemplateFilesRepositoryService { const template = await templateFilesRepo.findOne({ where: { account_id: accountId, file_name: fileName }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // 同名ファイルは同じものとして扱うため、すでにファイルがあれば更新(更新日時の履歴を残しておきたい) diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index ae3fccc..8c39441 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -289,6 +289,7 @@ export class UsersRepositoryService { const targetUser = await repo.findOne({ where: { id: id, account_id: accountId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理 From dbff86915efcde4a1449c4da75d7a9d5f11f630f Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 29 Jan 2024 05:54:35 +0000 Subject: [PATCH 07/18] =?UTF-8?q?Merged=20PR=20708:=20=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3550: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3550) - Worktypeの登録・編集時に16文字までとなるように制限を修正しました。 - APIのバリデータ - 画面の最大文字数 ## レビューポイント - 対応内容の認識はあっていますでしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- .../src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx | 2 +- .../src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx | 2 +- dictation_server/src/features/accounts/types/types.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx index 7f4934f..bf0f407 100644 --- a/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx @@ -85,7 +85,7 @@ export const AddWorktypeIdPopup: React.FC = ( { diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx index 248005b..a3d4fee 100644 --- a/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx @@ -84,7 +84,7 @@ export const EditWorktypeIdPopup: React.FC = ( { diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index e0202f7..2648785 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -198,7 +198,7 @@ export class CancelIssueRequest { export class CreateWorktypesRequest { @ApiProperty({ minLength: 1, maxLength: 255, description: 'WorktypeID' }) @MinLength(1) - @MaxLength(255) + @MaxLength(16) @IsRecorderAllowed() worktypeId: string; @ApiProperty({ description: 'Worktypeの説明', required: false }) @@ -210,7 +210,7 @@ export class CreateWorktypesRequest { export class UpdateWorktypesRequest { @ApiProperty({ minLength: 1, description: 'WorktypeID' }) @MinLength(1) - @MaxLength(255) + @MaxLength(16) @IsRecorderAllowed() worktypeId: string; @ApiProperty({ description: 'Worktypeの説明', required: false }) From f9d5082f39d125dae10d464a779686a03803605c Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Mon, 29 Jan 2024 08:38:26 +0000 Subject: [PATCH 08/18] =?UTF-8?q?Merged=20PR=20706:=20=E7=BF=BB=E8=A8=B3?= =?UTF-8?q?=E6=83=85=E5=A0=B1=E3=82=92=E5=8F=8D=E6=98=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3529: 翻訳情報を反映する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3529) - 翻訳情報を反映 ## レビューポイント - 特になし ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認、develop環境で確認など ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/translation/de.json | 38 ++++++++++++----------- dictation_client/src/translation/en.json | 17 ++++++----- dictation_client/src/translation/es.json | 39 ++++++++++++------------ dictation_client/src/translation/fr.json | 39 ++++++++++++------------ 4 files changed, 69 insertions(+), 64 deletions(-) diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 47716dd..4283e39 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -24,7 +24,7 @@ "headerDictations": "Diktate", "headerWorkflow": "Arbeitsablauf", "headerPartners": "Partner", - "headerSupport": "(de)Support", + "headerSupport": "Support", "tier1": "Admin", "tier2": "BC", "tier3": "Verteiler", @@ -76,6 +76,7 @@ "linkOfEula": "Klicken Sie hier, um die Endbenutzer-Lizenzvereinbarung zu lesen.", "linkOfPrivacyNotice": "Klicken Sie hier, um die Datenschutzerklärung zu lesen.", "forOdms": "für ODMS Cloud.", + "termsCheckBox": "Ja, ich stimme den Nutzungsbedingungen zu.", "createAccountButton": "Einreichen" } }, @@ -179,8 +180,8 @@ "storageSize": "Lagerung verfügbar", "usedSize": "Gebrauchter Lagerung", "storageAvailable": "Speicher nicht verfügbar (Menge überschritten)", - "licenseLabel": "(de)License", - "storageLabel": "(de)Storage" + "licenseLabel": "Lizenz", + "storageLabel": "Lagerung" } }, "licenseOrderPage": { @@ -206,8 +207,9 @@ "noPlaybackAuthorization": "Sie haben keine Berechtigung zum Abspielen dieser Datei.", "taskToPlaybackNoExists": "Die Datei kann nicht abgespielt werden, da sie bereits transkribiert wurde oder nicht existiert.", "taskNotEditable": "Der Transkriptionist kann nicht geändert werden, da die Transkription bereits ausgeführt wird oder die Datei nicht vorhanden ist. Bitte aktualisieren Sie den Bildschirm und prüfen Sie den aktuellen Status.", - "backupFailedError": "(de)ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", - "cancelFailedError": "(de)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。" + "backupFailedError": "Der Prozess „Dateisicherung“ ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal. Wenn der Fehler weiterhin besteht, wenden Sie sich an Ihren Systemadministrator.", + "cancelFailedError": "Die Diktate konnten nicht gelöscht werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.", + "deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" }, "label": { "title": "Diktate", @@ -250,9 +252,9 @@ "deleteDictation": "Diktat löschen", "selectedTranscriptionist": "Ausgewählter transkriptionist", "poolTranscriptionist": "Transkriptionsliste", - "fileBackup": "(de)File Backup", - "downloadForBackup": "(de)Download for backup", - "applications": "(de)Applications", + "fileBackup": "Dateisicherung", + "downloadForBackup": "Zur Sicherung herunterladen", + "applications": "Desktopanwendung", "cancelDictation": "Transkription abbrechen" } }, @@ -471,7 +473,7 @@ "addAccount": "Konto hinzufügen", "name": "Name der Firma", "category": "Kontoebene", - "accountId": "Konto-ID", + "accountId": "Autoren-ID", "country": "Land", "primaryAdmin": "Hauptadministrator", "email": "Email", @@ -538,22 +540,22 @@ }, "supportPage": { "label": { - "title": "(de)Support", - "howToUse": "(de)How to use the system", + "title": "Support", + "howToUse": "So verwenden Sie das System", "supportPageEnglish": "OMDS Cloud User Guide", - "supportPageGerman": "OMDS Cloud Benutzerhandbuch", - "supportPageFrench": "OMDS Cloud Mode d'emploi", - "supportPageSpanish": "OMDS Cloud Guía del usuario" + "supportPageGerman": "OMDS Cloud-Benutzerhandbuch", + "supportPageFrench": "Guía del usuario de la nube OMDS", + "supportPageSpanish": "Guide de l'utilisateur du cloud OMDS" }, "text": { - "notResolved": "(de)If the problem persists even after referring to the user guide, please contact a higher-level person in charge." + "notResolved": "Informationen zu den Funktionen der ODMS Cloud finden Sie im Benutzerhandbuch. Wenn Sie zusätzlichen Support benötigen, wenden Sie sich bitte an Ihren Administrator oder zertifizierten ODMS Cloud-Händler." } }, "filePropertyPopup": { "label": { - "general": "(de)General", - "job": "(de)Job", - "close": "(de)Close" + "general": "Allgemein", + "job": "Aufgabe", + "close": "Schließen" } } } diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index ae656b3..2a4baf1 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -75,7 +75,7 @@ "password": "Password", "linkOfEula": "Click here to read the End User License Agreement.", "linkOfPrivacyNotice": "Click here to read the Privacy Notice.", - "forOdms": "for ODMS Cloud.", + "forOdms": "for OMDS Cloud.", "termsCheckBox": "Yes, I agree to the terms of use.", "createAccountButton": "Submit" } @@ -207,8 +207,9 @@ "noPlaybackAuthorization": "You do not have permission to playback this file.", "taskToPlaybackNoExists": "The file cannot be played because it has already been transcribed or does not exist.", "taskNotEditable": "The transcriptionist cannot be changed because the transcription is already in progress or the file does not exist. Please refresh the screen and check the latest status.", - "backupFailedError": "ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", - "cancelFailedError": "タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。" + "backupFailedError": "The \"File Backup\" process has failed. Please try again later. If the error continues, contact your system administrator.", + "cancelFailedError": "Failed to delete the dictations. Please refresh your screen and try again.", + "deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" }, "label": { "title": "Dictations", @@ -253,7 +254,7 @@ "poolTranscriptionist": "Transcription List", "fileBackup": "File Backup", "downloadForBackup": "Download for backup", - "applications": "Applications", + "applications": "Desktop Application", "cancelDictation": "Cancel Transcription" } }, @@ -542,12 +543,12 @@ "title": "Support", "howToUse": "How to use the system", "supportPageEnglish": "OMDS Cloud User Guide", - "supportPageGerman": "OMDS Cloud Benutzerhandbuch", - "supportPageFrench": "OMDS Cloud Mode d'emploi", - "supportPageSpanish": "OMDS Cloud Guía del usuario" + "supportPageGerman": "OMDS Cloud-Benutzerhandbuch", + "supportPageFrench": "Guía del usuario de la nube OMDS", + "supportPageSpanish": "Guide de l'utilisateur du cloud OMDS" }, "text": { - "notResolved": "If the problem persists even after referring to the user guide, please contact a higher-level person in charge." + "notResolved": "Please refer to the User Guide for information about the features of the ODMS Cloud. If you require additional support, please contact your administrator or certified ODMS Cloud reseller." } }, "filePropertyPopup": { diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index bd228a5..43d4d44 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -24,7 +24,7 @@ "headerDictations": "Dictado", "headerWorkflow": "flujo de trabajo", "headerPartners": "Socios", - "headerSupport": "(es)Support", + "headerSupport": "Soporte", "tier1": "Admin", "tier2": "BC", "tier3": "Distribuidor", @@ -180,8 +180,8 @@ "storageSize": "Almacenamiento disponible", "usedSize": "Almacenamiento utilizado", "storageAvailable": "Almacenamiento no disponible (cantidad excedida)", - "licenseLabel": "(es)License", - "storageLabel": "(es)Storage" + "licenseLabel": "Licencia", + "storageLabel": "Almacenamiento" } }, "licenseOrderPage": { @@ -207,8 +207,9 @@ "noPlaybackAuthorization": "No tienes permiso para reproducir este archivo.", "taskToPlaybackNoExists": "El archivo no se puede reproducir porque ya ha sido transcrito o no existe.", "taskNotEditable": "No se puede cambiar el transcriptor porque la transcripción ya está en curso o el archivo no existe. Actualice la pantalla y verifique el estado más reciente.", - "backupFailedError": "(es)ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", - "cancelFailedError": "(es)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。" + "backupFailedError": "El proceso de \"Copia de seguridad de archivos\" ha fallado. Por favor, inténtelo de nuevo más tarde. Si el error continúa, comuníquese con el administrador del sistema.", + "cancelFailedError": "No se pudieron eliminar los dictados. Actualice su pantalla e inténtelo nuevamente.", + "deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" }, "label": { "title": "Dictado", @@ -251,9 +252,9 @@ "deleteDictation": "Borrar dictado", "selectedTranscriptionist": "Transcriptor seleccionado", "poolTranscriptionist": "Lista de transcriptor", - "fileBackup": "(es)File Backup", - "downloadForBackup": "(es)Download for backup", - "applications": "(es)Applications", + "fileBackup": "Copia de seguridad de archivos", + "downloadForBackup": "Descargar para respaldo", + "applications": "Aplicación de escritorio", "cancelDictation": "Cancelar transcripción" } }, @@ -472,11 +473,11 @@ "addAccount": "Añadir cuenta", "name": "Nombre de empresa", "category": "Nivel de cuenta", - "accountId": "ID de la cuenta", + "accountId": "ID de autor", "country": "País", "primaryAdmin": "Administrador primario", "email": "Email", - "dealerManagement": "Permitir que el concesionario realice los cambios", + "dealerManagement": "Permitir que el distribuidor realice los cambios", "partners": "Socios", "deleteAccount": "Borrar cuenta" }, @@ -539,22 +540,22 @@ }, "supportPage": { "label": { - "title": "(es)Support", - "howToUse": "(es)How to use the system", + "title": "Soporte", + "howToUse": "Cómo utilizar el sistema", "supportPageEnglish": "OMDS Cloud User Guide", - "supportPageGerman": "OMDS Cloud Benutzerhandbuch", - "supportPageFrench": "OMDS Cloud Mode d'emploi", - "supportPageSpanish": "OMDS Cloud Guía del usuario" + "supportPageGerman": "OMDS Cloud-Benutzerhandbuch", + "supportPageFrench": "Guía del usuario de la nube OMDS", + "supportPageSpanish": "Guide de l'utilisateur du cloud OMDS" }, "text": { - "notResolved": "(es)If the problem persists even after referring to the user guide, please contact a higher-level person in charge." + "notResolved": "Consulte la Guía del usuario para obtener información sobre las funciones de ODMS Cloud. Si necesita soporte adicional, comuníquese con su administrador o revendedor certificado de ODMS Cloud." } }, "filePropertyPopup": { "label": { - "general": "(es)General", - "job": "(es)Job", - "close": "(es)Close" + "general": "General", + "job": "Trabajo", + "close": "Cerrar" } } } diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index f068d3d..57c5da9 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -24,7 +24,7 @@ "headerDictations": "Dictées", "headerWorkflow": "Flux de travail", "headerPartners": "Partenaires", - "headerSupport": "(fr)Support", + "headerSupport": "Support", "tier1": "Admin", "tier2": "BC", "tier3": "Distributeur", @@ -180,8 +180,8 @@ "storageSize": "Stockage disponible", "usedSize": "Stockage utilisé", "storageAvailable": "Stockage indisponible (montant dépassée)", - "licenseLabel": "(fr)License", - "storageLabel": "(fr)Storage" + "licenseLabel": "Licence", + "storageLabel": "Stockage" } }, "licenseOrderPage": { @@ -207,8 +207,9 @@ "noPlaybackAuthorization": "Vous n'êtes pas autorisé à lire ce fichier.", "taskToPlaybackNoExists": "Le fichier ne peut pas être lu car il a déjà été transcrit ou n'existe pas.", "taskNotEditable": "Le transcripteur ne peut pas être changé car la transcription est déjà en cours ou le fichier n'existe pas. Veuillez actualiser l'écran et vérifier le dernier statut.", - "backupFailedError": "(fr)ファイルのバックアップに失敗したため処理を中断しました。時間をおいて再実行しても解決しない場合はシステム管理者にお問い合わせください。", - "cancelFailedError": "(fr)タスクのキャンセルに失敗しました。画面を更新し、再度ご確認ください。" + "backupFailedError": "Le processus de « Sauvegarde de fichier » a échoué. Veuillez réessayer plus tard. Si l'erreur persiste, contactez votre administrateur système.", + "cancelFailedError": "Échec de la suppression des dictées. Veuillez actualiser votre écran et réessayer.", + "deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" }, "label": { "title": "Dictées", @@ -251,9 +252,9 @@ "deleteDictation": "Supprimer la dictée", "selectedTranscriptionist": "Transcriptionniste sélectionné", "poolTranscriptionist": "Liste de transcriptionniste", - "fileBackup": "(fr)File Backup", - "downloadForBackup": "(fr)Download for backup", - "applications": "(fr)Applications", + "fileBackup": "Sauvegarde de fichiers", + "downloadForBackup": "Télécharger pour sauvegarde", + "applications": "Application de bureau", "cancelDictation": "Annuler la transcription" } }, @@ -472,11 +473,11 @@ "addAccount": "Ajouter compte", "name": "Nom de l'entreprise", "category": "Niveau compte", - "accountId": "identifiant de compte", + "accountId": "Identifiant Auteur", "country": "Pays", "primaryAdmin": "Administrateur principal", "email": "Email", - "dealerManagement": "Autoriser le concessionnaire à modifier les paramètres", + "dealerManagement": "Autoriser le revendeur à modifier les paramètres", "partners": "Partenaires", "deleteAccount": "Supprimer le compte" }, @@ -539,22 +540,22 @@ }, "supportPage": { "label": { - "title": "(fr)Support", - "howToUse": "(fr)How to use the system", + "title": "Support", + "howToUse": "Comment utiliser le système", "supportPageEnglish": "OMDS Cloud User Guide", - "supportPageGerman": "OMDS Cloud Benutzerhandbuch", - "supportPageFrench": "OMDS Cloud Mode d'emploi", - "supportPageSpanish": "OMDS Cloud Guía del usuario" + "supportPageGerman": "OMDS Cloud-Benutzerhandbuch", + "supportPageFrench": "Guía del usuario de la nube OMDS", + "supportPageSpanish": "Guide de l'utilisateur du cloud OMDS" }, "text": { - "notResolved": "(fr)If the problem persists even after referring to the user guide, please contact a higher-level person in charge." + "notResolved": "Veuillez vous référer au Guide de l'utilisateur pour plus d'informations sur les fonctionnalités d'ODMS Cloud. Si vous avez besoin d'une assistance supplémentaire, veuillez contacter votre administrateur ou votre revendeur certifié ODMS Cloud." } }, "filePropertyPopup": { "label": { - "general": "(fr)General", - "job": "(fr)Job", - "close": "(fr)Close" + "general": "Général", + "job": "Tâches", + "close": "Fermer" } } } From 6d6eee91e02dcae0f3a62bcaf302d66ceecfba21 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 31 Jan 2024 00:57:36 +0000 Subject: [PATCH 09/18] =?UTF-8?q?Merged=20PR=20712:=20=E8=A1=8C=E3=83=AD?= =?UTF-8?q?=E3=83=83=E3=82=AF=E6=A8=AA=E5=B1=95=E9=96=8B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3470: 行ロック横展開2](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3470) 以下のリポジトリの各メソッドについてロックを追加し、必要なインデックスを追加するマイグレーションファイルを追加しました。 - licenses - order - 注文の取得にロックを追加して同じPO番号をチェックできるようにする - createCardLicenses - ライセンスキーチェック毎にロックを追加して同じライセンスキーをチェックできるようにする - activateCardLicense - ライセンスキーのチェックにロックを追加して同一のライセンスキーがアクティベート済みかチェックできるようにする - issueLicense - 注文のチェックにロックを追加して同一の注文に対して複数回ライセンスが発行されないようにする - allocateLicense - ユーザーのライセンス状態取得にロックを追加して複数回割り当てできないようにする - deallocateLicense - ユーザーのライセンス状態取得にロックを追加して複数回解除できないようにする - cancelOrder - 注文のチェックにロックを追加してキャンセル中にライセンスが発行されないようにする ※こちらの資料を参考に各メソッド内で影響に関連すると思われるselectにロックを追加しています。 [行ロックに関する影響調査.xlsx](https://ndstokyo.sharepoint.com/:x:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E8%A1%8C%E3%83%AD%E3%83%83%E3%82%AF%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E5%BD%B1%E9%9F%BF%E8%AA%BF%E6%9F%BB.xlsx?d=wdd6f3d97f7b04a538095c459f8eee2eb&csf=1&web=1&e=qASAOx) 上記資料を参考にタスク内で担当するメソッドについてロックの対応箇所を整理しました。 [Task3470](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/%E3%83%A9%E3%82%A4%E3%82%BB%E3%83%B3%E3%82%B9%E3%83%9D%E3%83%BC%E3%82%BF%E3%83%AB/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3470?csf=1&web=1&e=OGnOhp) ## レビューポイント - 各メソッドの対応方針は適切でしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- .../db/migrations/054-add_license_index.sql | 13 +++++++++++++ .../licenses/licenses.repository.service.ts | 6 ++++++ 2 files changed, 19 insertions(+) create mode 100644 dictation_server/db/migrations/054-add_license_index.sql diff --git a/dictation_server/db/migrations/054-add_license_index.sql b/dictation_server/db/migrations/054-add_license_index.sql new file mode 100644 index 0000000..f08b2e9 --- /dev/null +++ b/dictation_server/db/migrations/054-add_license_index.sql @@ -0,0 +1,13 @@ +-- +migrate Up +ALTER TABLE `license_orders` ADD INDEX `idx_po_number` (po_number); +ALTER TABLE `license_orders` ADD INDEX `idx_from_account_id` (from_account_id); +ALTER TABLE `license_orders` ADD INDEX `idx_status` (status); +ALTER TABLE `card_licenses` ADD INDEX `idx_card_license_key` (card_license_key); +ALTER TABLE `licenses` ADD INDEX `idx_status` (status); + +-- +migrate Down +ALTER TABLE `license_orders` DROP INDEX `idx_po_number`; +ALTER TABLE `license_orders` DROP INDEX `idx_from_account_id`; +ALTER TABLE `license_orders` DROP INDEX `idx_status`; +ALTER TABLE `card_licenses` DROP INDEX `idx_card_license_key`; +ALTER TABLE `licenses` DROP INDEX `idx_status`; \ No newline at end of file diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index a5e03a3..2a0c328 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -81,6 +81,7 @@ export class LicensesRepositoryService { }, ], comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // 重複があった場合はエラーを返却する if (isPoNumberDuplicated) { @@ -193,6 +194,7 @@ export class LicensesRepositoryService { card_license_key: In(generateKeys), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (existingCardLicenses.length > 0) { // 重複分を配列から削除 @@ -292,6 +294,7 @@ export class LicensesRepositoryService { card_license_key: licenseKey, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // カードライセンスが存在しなければエラー if (!targetCardLicense) { @@ -602,6 +605,7 @@ export class LicensesRepositoryService { allocated_user_id: userId, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // 既にライセンスが割り当てられているなら、割り当てを解除 @@ -717,6 +721,7 @@ export class LicensesRepositoryService { status: LICENSE_ALLOCATED_STATUS.ALLOCATED, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // ライセンスが割り当てられていない場合はエラー @@ -776,6 +781,7 @@ export class LicensesRepositoryService { status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // キャンセル対象の注文が存在しない場合エラー From e877942175d22a38ec09536b167ca3ac3e77a507 Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Thu, 1 Feb 2024 06:06:10 +0000 Subject: [PATCH 10/18] =?UTF-8?q?Merged=20PR=20711:=20Repository=E3=83=AD?= =?UTF-8?q?=E3=83=83=E3=82=AF=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3523: Repositoryロック対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3523) LicensesRepository - allocateLicense 割当先ユーザ取得しロックする処理を追加 WorkflowRepository - createtWorkflows authorの存在確認時にロックを追加 - updatetWorkflows authorの存在確認時にロックを追加 UserGroup&UserGroupMemberのロックを追加 AccountsRepository - updateAccountInfo プライマリ/セカンダリ管理者ユーザーの存在チェックのロック (行ロック横展開1で修正されていた) TasksRepository - create タスクの所有者の存在確認とロックを追加 - checkout 対象ユーザの存在確認とロックを追加 - changeCheckoutPermission 対象ユーザの存在確認とロックを追加 UserGroupsRepository - createTypistGroup 対象ユーザ達のロックを追加 - updateTypistGroup 対象ユーザ達のロックを追加 ## レビューポイント ラフスケッチの、 ``` 競合ケース E-3. Typistが削除条件判定を行った直後に、チェックアウト候補に削除ユーザーが含まれるTypistGroupが割り当てられる TypistGroupに割り当たっている時点で削除条件を満たさないので、このケースはないはず ``` ここは未対応でよい認識か。 ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- .../licenses/licenses.repository.service.ts | 15 +++++ .../tasks/tasks.repository.service.ts | 32 ++++++++++ .../user_groups.repository.service.ts | 2 + .../workflows/workflows.repository.service.ts | 63 ++++++++++--------- 4 files changed, 82 insertions(+), 30 deletions(-) diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 2a0c328..6270b43 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -39,6 +39,8 @@ import { updateEntity, } from '../../common/repository'; import { Context } from '../../common/log'; +import { User } from '../users/entity/user.entity'; +import { UserNotFoundError } from '../users/errors/types'; @Injectable() export class LicensesRepositoryService { @@ -559,6 +561,19 @@ export class LicensesRepositoryService { accountId: number, ): Promise { await this.dataSource.transaction(async (entityManager) => { + // 対象ユーザの存在チェック + const userRepo = entityManager.getRepository(User); + const user = await userRepo.findOne({ + where: { + id: userId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + if (!user) { + throw new UserNotFoundError(`User not exist. userId: ${userId}`); + } + const licenseRepo = entityManager.getRepository(License); const licenseAllocationHistoryRepo = entityManager.getRepository( LicenseAllocationHistory, diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 9ac0e48..824a85c 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -48,6 +48,7 @@ import { deleteEntity, } from '../../common/repository'; import { Context } from '../../common/log'; +import { UserNotFoundError } from '../users/errors/types'; @Injectable() export class TasksRepositoryService { @@ -167,6 +168,20 @@ export class TasksRepositoryService { permittedSourceStatus: TaskStatus[], ): Promise { await this.dataSource.transaction(async (entityManager) => { + // 対象ユーザの存在確認 + const userRepo = entityManager.getRepository(User); + const user = await userRepo.findOne({ + where: { + id: user_id, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + if (!user) { + throw new TypistUserNotFoundError( + `Typist user not exists. user_id:${user_id}`, + ); + } const taskRepo = entityManager.getRepository(Task); // 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得 const task = await taskRepo.findOne({ @@ -846,6 +861,22 @@ export class TasksRepositoryService { const createdEntity = await this.dataSource.transaction( async (entityManager) => { + // タスクの所有者の存在確認 + const userRepo = entityManager.getRepository(User); + const user = await userRepo.findOne({ + where: { + id: owner_user_id, + account_id: account_id, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + if (!user) { + throw new UserNotFoundError( + `User not exists. owner_user_id:${owner_user_id}`, + ); + } + const audioFileRepo = entityManager.getRepository(AudioFile); const newAudioFile = audioFileRepo.create(audioFile); const savedAudioFile = await insertEntity( @@ -967,6 +998,7 @@ export class TasksRepositoryService { deleted_at: IsNull(), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // idはユニークであるため取得件数の一致でユーザーの存在を確認 if (typistUserIds.length !== userRecords.length) { diff --git a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts index eca0bc7..e7db5cf 100644 --- a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts +++ b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts @@ -123,6 +123,7 @@ export class UserGroupsRepositoryService { email_verified: true, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (userRecords.length !== typistIds.length) { throw new TypistIdInvalidError( @@ -189,6 +190,7 @@ export class UserGroupsRepositoryService { email_verified: true, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (userRecords.length !== typistIds.length) { throw new TypistIdInvalidError( diff --git a/dictation_server/src/repositories/workflows/workflows.repository.service.ts b/dictation_server/src/repositories/workflows/workflows.repository.service.ts index cf28006..de5e38e 100644 --- a/dictation_server/src/repositories/workflows/workflows.repository.service.ts +++ b/dictation_server/src/repositories/workflows/workflows.repository.service.ts @@ -87,6 +87,7 @@ export class WorkflowsRepositoryService { const author = await userRepo.findOne({ where: { account_id: accountId, id: authorId, email_verified: true }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!author) { throw new UserNotFoundError( @@ -227,6 +228,37 @@ export class WorkflowsRepositoryService { ): Promise { return await this.dataSource.transaction(async (entityManager) => { const workflowRepo = entityManager.getRepository(Workflow); + // authorの存在確認 + const userRepo = entityManager.getRepository(User); + const author = await userRepo.findOne({ + where: { account_id: accountId, id: authorId, email_verified: true }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + if (!author) { + throw new UserNotFoundError( + `author not found or email not verified. id: ${authorId}`, + ); + } + + // ルーティング候補ユーザーの存在確認 + 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({ @@ -239,18 +271,6 @@ export class WorkflowsRepositoryService { ); } - // authorの存在確認 - const userRepo = entityManager.getRepository(User); - const author = await userRepo.findOne({ - where: { account_id: accountId, id: authorId, email_verified: true }, - comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, - }); - if (!author) { - throw new UserNotFoundError( - `author not found or email not verified. id: ${authorId}`, - ); - } - // worktypeの存在確認 if (worktypeId !== undefined) { const worktypeRepo = entityManager.getRepository(Worktype); @@ -279,24 +299,6 @@ export class WorkflowsRepositoryService { } } - // ルーティング候補ユーザーの存在確認 - const typistIds = typists.flatMap((typist) => - typist.typistId ? [typist.typistId] : [], - ); - const typistUsers = await userRepo.find({ - where: { - account_id: accountId, - id: In(typistIds), - email_verified: true, - }, - comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, - }); - if (typistUsers.length !== typistIds.length) { - throw new UserNotFoundError( - `typist not found or email not verified. ids: ${typistIds}`, - ); - } - // ルーティング候補ユーザーグループの存在確認 const groupIds = typists.flatMap((typist) => { return typist.typistGroupId ? [typist.typistGroupId] : []; @@ -305,6 +307,7 @@ export class WorkflowsRepositoryService { const typistGroups = await userGroupRepo.find({ where: { account_id: accountId, id: In(groupIds) }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (typistGroups.length !== groupIds.length) { throw new TypistGroupNotExistError( From 06b5249e5ac48df6eeba024e38f5d295e5a68010 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Fri, 2 Feb 2024 02:00:47 +0000 Subject: [PATCH 11/18] =?UTF-8?q?Merged=20PR=20717:=20[FB=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C]=E7=94=BB=E9=9D=A2=E6=9B=B4=E6=96=B0=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=81=A8=E3=83=98=E3=83=83=E3=83=80=E3=83=BC=E3=81=AE?= =?UTF-8?q?=E3=82=BD=E3=83=BC=E3=83=88=E3=81=8CJob=20number=E3=81=AB?= =?UTF-8?q?=E6=88=BB=E3=81=A3=E3=81=A6=E3=81=97=E3=81=BE=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3611: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3611) - ソート条件を変更した際にローカルストレージに保持するように修正 - 例 `direction:ASC,paramName:RECORDING_STARTED_DATE` - ローカルストレージにソート条件が入っていれば、その条件でソートしたタスクを取得するように修正 - PlayBack時にローカルストレージにあるソート条件を削除するように修正 - 削除することで、次回の画面初期表示時はPlayBackを押したときのソート条件を使用することができる。 - PlayBack時にユーザーがタイピストの時のみソート条件を保存していたがその制限は不要そうだったのでAuthorでもソート条件を更新するように修正。 - AuthorがPlayBack押下時にソート条件を更新しても不都合はないため。 ## レビューポイント - ローカルストレージに保存する処理を入れる箇所に問題はないか ## UIの変更 -https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3611?csf=1&web=1&e=5uG6f4 ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- .../src/components/auth/constants.ts | 7 +++- .../src/features/dictation/constants.ts | 11 +++++ .../src/features/dictation/operations.ts | 18 ++++----- .../src/pages/DictationPage/index.tsx | 40 +++++++++++++++++-- 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/dictation_client/src/components/auth/constants.ts b/dictation_client/src/components/auth/constants.ts index ef98041..dd8ec52 100644 --- a/dictation_client/src/components/auth/constants.ts +++ b/dictation_client/src/components/auth/constants.ts @@ -43,7 +43,12 @@ export const UNAUTHORIZED_TO_CONTINUE_ERROR_CODES = [ * ローカルストレージに残すキー類 * @const {string[]} */ -export const KEYS_TO_PRESERVE = ["accessToken", "refreshToken", "displayInfo"]; +export const KEYS_TO_PRESERVE = [ + "accessToken", + "refreshToken", + "displayInfo", + "sortCriteria", +]; /** * アクセストークンを更新する基準の秒数 diff --git a/dictation_client/src/features/dictation/constants.ts b/dictation_client/src/features/dictation/constants.ts index 69eae60..f7e22d1 100644 --- a/dictation_client/src/features/dictation/constants.ts +++ b/dictation_client/src/features/dictation/constants.ts @@ -28,6 +28,13 @@ export const SORTABLE_COLUMN = { export type SortableColumnType = typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; +export const isSortableColumnType = ( + value: string +): value is SortableColumnType => { + const arg = value as SortableColumnType; + return Object.values(SORTABLE_COLUMN).includes(arg); +}; + export type SortableColumnList = typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; @@ -38,6 +45,10 @@ export const DIRECTION = { export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION]; +// DirectionTypeの型チェック関数 +export const isDirectionType = (arg: string): arg is DirectionType => + arg in DIRECTION; + export interface DisplayInfoType { JobNumber: boolean; Status: boolean; diff --git a/dictation_client/src/features/dictation/operations.ts b/dictation_client/src/features/dictation/operations.ts index 23b98f4..99b426e 100644 --- a/dictation_client/src/features/dictation/operations.ts +++ b/dictation_client/src/features/dictation/operations.ts @@ -280,7 +280,6 @@ export const playbackAsync = createAsyncThunk< direction: DirectionType; paramName: SortableColumnType; audioFileId: number; - isTypist: boolean; }, { // rejectした時の返却値の型 @@ -289,7 +288,7 @@ export const playbackAsync = createAsyncThunk< }; } >("dictations/playbackAsync", async (args, thunkApi) => { - const { audioFileId, direction, paramName, isTypist } = args; + const { audioFileId, direction, paramName } = args; // apiのConfigurationを取得する const { getState } = thunkApi; @@ -300,15 +299,12 @@ export const playbackAsync = createAsyncThunk< const tasksApi = new TasksApi(config); const usersApi = new UsersApi(config); try { - // ユーザーがタイピストである場合に、ソート条件を保存する - if (isTypist) { - await usersApi.updateSortCriteria( - { direction, paramName }, - { - headers: { authorization: `Bearer ${accessToken}` }, - } - ); - } + await usersApi.updateSortCriteria( + { direction, paramName }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); await tasksApi.checkout(audioFileId, { headers: { authorization: `Bearer ${accessToken}` }, }); diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx index bb70c47..cbeed07 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -33,6 +33,8 @@ import { playbackAsync, cancelAsync, PRIORITY, + isSortableColumnType, + isDirectionType, } from "features/dictation"; import { getTranslationID } from "translation"; import { Task } from "api/api"; @@ -242,6 +244,12 @@ const DictationPage: React.FC = (): JSX.Element => { dispatch(changeDirection({ direction: currentDirection })); dispatch(changeParamName({ paramName })); + // ローカルストレージにソート情報を保存する + localStorage.setItem( + "sortCriteria", + `direction:${currentDirection},paramName:${paramName}` + ); + const filter = getFilter( filterUploaded, filterInProgress, @@ -348,10 +356,11 @@ const DictationPage: React.FC = (): JSX.Element => { audioFileId, direction: sortDirection, paramName: sortableParamName, - isTypist, }) ); if (meta.requestStatus === "fulfilled") { + // ローカルストレージにソート情報を削除する + localStorage.removeItem("sortCriteria"); const filter = getFilter( filterUploaded, filterInProgress, @@ -388,7 +397,6 @@ const DictationPage: React.FC = (): JSX.Element => { filterInProgress, filterPending, filterUploaded, - isTypist, sortDirection, sortableParamName, t, @@ -522,13 +530,39 @@ const DictationPage: React.FC = (): JSX.Element => { dispatch(changeDisplayInfo({ column: displayInfo })); const filter = getFilter(true, true, true, true, false); + const { meta, payload } = await dispatch(getSortColumnAsync()); if ( meta.requestStatus === "fulfilled" && payload && !("error" in payload) ) { - const { direction, paramName } = payload; + // ソート情報をローカルストレージから取得する + const sortColumnValue = localStorage.getItem("sortCriteria") ?? ""; + let direction: DirectionType; + let paramName: SortableColumnType; + if (sortColumnValue === "") { + direction = payload.direction; + paramName = payload.paramName; + } else { + // ソート情報をDirectionとParamNameに分割する + const sortColumn = sortColumnValue?.split(","); + const localStorageDirection = sortColumn[0].split(":")[1] ?? ""; + + const localStorageParamName = sortColumn[1]?.split(":")[1] ?? ""; + + // 正常なソート情報がローカルストレージに存在する場合はローカルストレージの情報を使用する + direction = isDirectionType(localStorageDirection) + ? localStorageDirection + : payload.direction; + paramName = isSortableColumnType(localStorageParamName) + ? localStorageParamName + : payload.paramName; + + dispatch(changeDirection({ direction })); + dispatch(changeParamName({ paramName })); + } + dispatch( listTasksAsync({ limit: LIMIT_TASK_NUM, From 4548b5e5107bd941db0f177f2d5f636abd949b49 Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Fri, 2 Feb 2024 02:37:39 +0000 Subject: [PATCH 12/18] =?UTF-8?q?Merged=20PR=20715:=20=E8=A1=8C=E3=83=AD?= =?UTF-8?q?=E3=83=83=E3=82=AF=E6=A8=AA=E5=B1=95=E9=96=8B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3472: 行ロック横展開4](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3472) - 対象メソッド - user_groups - createTypistGroup - グループに含めるユーザー情報取得箇所でロック追加 - ユーザー削除と被ると、削除済みユーザーをユーザーグループに含めてしまう - updateTypistGroup - グループに含めるユーザー情報取得箇所でロック追加 - ユーザー削除と被ると、削除済みユーザーをユーザーグループに含めてしまう - グループの存在確認を行う箇所 - グループ削除と被ると、削除済みのグループにメンバーを割り当ててしまう - workflows - createtWorkflows - updatetWorkflow - インデックス追加 - user - role ## レビューポイント - インデックスの貼り忘れはないか - ロックの追加忘れはないか ## 共有資料 - https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3472?csf=1&web=1&e=jjb0QV ## 動作確認状況 - ローカルでロックされている箇所で待ちが発生していることを確認 ## 補足 - 相談、参考資料などがあれば --- dictation_server/db/migrations/055-add_users_index.sql | 5 +++++ .../user_groups/user_groups.repository.service.ts | 5 +++-- .../workflows/workflows.repository.service.ts | 9 +++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 dictation_server/db/migrations/055-add_users_index.sql diff --git a/dictation_server/db/migrations/055-add_users_index.sql b/dictation_server/db/migrations/055-add_users_index.sql new file mode 100644 index 0000000..ea50c9d --- /dev/null +++ b/dictation_server/db/migrations/055-add_users_index.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE `users` ADD INDEX `idx_role` (role); + +-- +migrate Down +ALTER TABLE `users` DROP INDEX `idx_role`; \ No newline at end of file diff --git a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts index e7db5cf..050d3e1 100644 --- a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts +++ b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts @@ -122,8 +122,8 @@ export class UserGroupsRepositoryService { role: USER_ROLES.TYPIST, email_verified: true, }, - comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); if (userRecords.length !== typistIds.length) { throw new TypistIdInvalidError( @@ -189,8 +189,8 @@ export class UserGroupsRepositoryService { role: USER_ROLES.TYPIST, email_verified: true, }, - comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); if (userRecords.length !== typistIds.length) { throw new TypistIdInvalidError( @@ -206,6 +206,7 @@ export class UserGroupsRepositoryService { id: typistGroupId, account_id: accountId, }, + lock: { mode: 'pessimistic_write' }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); if (!typistGroup) { diff --git a/dictation_server/src/repositories/workflows/workflows.repository.service.ts b/dictation_server/src/repositories/workflows/workflows.repository.service.ts index de5e38e..2ccaf5e 100644 --- a/dictation_server/src/repositories/workflows/workflows.repository.service.ts +++ b/dictation_server/src/repositories/workflows/workflows.repository.service.ts @@ -101,6 +101,7 @@ export class WorkflowsRepositoryService { const worktypes = await worktypeRepo.find({ where: { account_id: accountId, id: worktypeId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (worktypes.length === 0) { throw new WorktypeIdNotFoundError( @@ -115,6 +116,7 @@ export class WorkflowsRepositoryService { const template = await templateRepo.findOne({ where: { account_id: accountId, id: templateId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!template) { throw new TemplateFileNotExistError('template not found.'); @@ -132,6 +134,7 @@ export class WorkflowsRepositoryService { email_verified: true, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (typistUsers.length !== typistIds.length) { throw new UserNotFoundError( @@ -147,6 +150,7 @@ export class WorkflowsRepositoryService { const typistGroups = await userGroupRepo.find({ where: { account_id: accountId, id: In(groupIds) }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (typistGroups.length !== groupIds.length) { throw new TypistGroupNotExistError( @@ -164,6 +168,7 @@ export class WorkflowsRepositoryService { worktype_id: worktypeId !== undefined ? worktypeId : IsNull(), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (workflow.length !== 0) { throw new AuthorIdAndWorktypeIdPairAlreadyExistsError( @@ -264,6 +269,7 @@ export class WorkflowsRepositoryService { 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( @@ -277,6 +283,7 @@ export class WorkflowsRepositoryService { const worktypes = await worktypeRepo.find({ where: { account_id: accountId, id: worktypeId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (worktypes.length === 0) { throw new WorktypeIdNotFoundError( @@ -291,6 +298,7 @@ export class WorkflowsRepositoryService { const template = await templateRepo.findOne({ where: { account_id: accountId, id: templateId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!template) { throw new TemplateFileNotExistError( @@ -402,6 +410,7 @@ export class WorkflowsRepositoryService { const workflow = await workflowRepo.findOne({ where: { account_id: accountId, id: workflowId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!workflow) { throw new WorkflowNotFoundError( From d5178e743504f41f9090a0f200cdc031ae43d069 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 5 Feb 2024 00:21:57 +0000 Subject: [PATCH 13/18] =?UTF-8?q?Merged=20PR=20716:=20=E8=A1=8C=E3=83=AD?= =?UTF-8?q?=E3=83=83=E3=82=AF=E6=A8=AA=E5=B1=95=E9=96=8B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3473: 行ロック横展開5](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3473) 以下のリポジトリのメソッドについてlockを追加しました。 - worktypes - createWorktype - ワークタイプ取得にロックを追加して途中で追加できないようにする - updateWorktype - ワークタイプ取得にロックを追加して途中で同一ワークタイプIDが作られないようにする - deleteWorktype - ワークフロー取得にロックを追加して途中でワークタイプが紐づけられないようにする - updateOptionItems - ワークタイプ取得にロックを追加して同一ワークタイプに対してオプションアイテムを作らないようにする こちらの資料を参考にして対応しています。 [行ロックに関する影響調査](https://ndstokyo.sharepoint.com/:x:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E8%A1%8C%E3%83%AD%E3%83%83%E3%82%AF%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E5%BD%B1%E9%9F%BF%E8%AA%BF%E6%9F%BB.xlsx?d=wdd6f3d97f7b04a538095c459f8eee2eb&csf=1&web=1&e=Y5l3aA) 対応内容についてはこちらにまとめています。 [行ロック5の対応整理](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3473?csf=1&web=1&e=wqCx0Z) ## レビューポイント - 対応箇所は適切でしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- .../repositories/worktypes/worktypes.repository.service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts b/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts index f647288..07e1704 100644 --- a/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts +++ b/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts @@ -88,6 +88,7 @@ export class WorktypesRepositoryService { const duplicatedWorktype = await worktypeRepo.findOne({ where: { account_id: accountId, custom_worktype_id: worktypeId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // ワークタイプIDが重複している場合はエラー @@ -100,6 +101,7 @@ export class WorktypesRepositoryService { const worktypeCount = await worktypeRepo.count({ where: { account_id: accountId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // ワークタイプの登録数が上限に達している場合はエラー @@ -163,6 +165,7 @@ export class WorktypesRepositoryService { const worktype = await worktypeRepo.findOne({ where: { account_id: accountId, id: id }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // ワークタイプが存在しない場合はエラー @@ -177,6 +180,7 @@ export class WorktypesRepositoryService { id: Not(id), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // ワークタイプIDが重複している場合はエラー @@ -227,6 +231,7 @@ export class WorktypesRepositoryService { const account = await accountRepo.findOne({ where: { id: accountId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (account?.active_worktype_id === id) { @@ -244,6 +249,7 @@ export class WorktypesRepositoryService { const workflows = await workflowRepo.find({ where: { account_id: accountId, worktype_id: id }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (workflows.length > 0) { const workflowIds = workflows.map((workflow) => workflow.id); @@ -322,6 +328,7 @@ export class WorktypesRepositoryService { const worktype = await worktypeRepo.findOne({ where: { account_id: accountId, id: worktypeId }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // ワークタイプが存在しない場合はエラー if (!worktype) { From 1daeedbfdb8e22a01b3d77c1af2022c20938fe2c Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Mon, 5 Feb 2024 06:11:41 +0000 Subject: [PATCH 14/18] =?UTF-8?q?Merged=20PR=20722:=20=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E7=94=BB=E9=9D=A2=E3=81=AB=E6=B3=A8?= =?UTF-8?q?=E9=87=88=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B=EF=BC=86?= =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E5=8F=8D=E6=98=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3592: アカウント画面に注釈を追加する&翻訳反映](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3592) - ディーラーマネジメントの下、保存ボタンの下に注釈追加 - タスクの中のBacklogに詳細があります - 翻訳反映 ## レビューポイント - 特になし ## UIの変更 - https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3592?csf=1&web=1&e=1wBNB6 ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- .../src/pages/AccountPage/index.tsx | 52 +++++++++++++------ dictation_client/src/styles/app.module.scss | 37 ++++++++++--- .../src/styles/app.module.scss.d.ts | 1 + dictation_client/src/translation/de.json | 14 ++++- dictation_client/src/translation/en.json | 16 ++++-- dictation_client/src/translation/es.json | 30 +++++++---- dictation_client/src/translation/fr.json | 30 +++++++---- 7 files changed, 132 insertions(+), 48 deletions(-) diff --git a/dictation_client/src/pages/AccountPage/index.tsx b/dictation_client/src/pages/AccountPage/index.tsx index 6fd0877..8e3a149 100644 --- a/dictation_client/src/pages/AccountPage/index.tsx +++ b/dictation_client/src/pages/AccountPage/index.tsx @@ -191,23 +191,32 @@ const AccountPage: React.FC = (): JSX.Element => { )} {isTier5 && ( -
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - -
+ <> +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +
+
+ {t( + getTranslationID( + "accountPage.text.dealerManagementAnnotation" + ) + )} +
+ )} {!isTier5 &&
-
} @@ -374,6 +383,15 @@ const AccountPage: React.FC = (): JSX.Element => { className={styles.icLoading} alt="Loading" /> + {isTier5 && ( +

+ {t( + getTranslationID( + "accountPage.text.dealerManagementAnnotation" + ) + )} +

+ )} {isTier5 && ( diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index 2435346..52f8553 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -1632,8 +1632,31 @@ _:-ms-lang(x)::-ms-backdrop, .account .listVertical dd .formInput { max-width: 100%; } +.account .listVertical dd.full { + width: 100%; + padding-top: 0; + background: none; +} +.account .listVertical dd.full.odd { + background: #f5f5f5; +} +.account .listVertical dd.formComment { + text-align: left; + font-size: 0.9rem; + word-break: break-word; +} +.account .box100 .formComment { + display: block; + width: 600px; + text-align: left; +} .account .box100.alignRight { width: calc(1200px + 3rem); + text-align: right; +} +.account .box100.alignRight .formComment { + margin-left: 648px; + text-align: right; } .menuAction { @@ -2306,7 +2329,8 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input + label:hover, .formChange ul.holdMember li input + label:hover { - background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center; + background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left + center; background-size: 1.3rem; } .formChange ul.chooseMember li input:checked + label, @@ -2317,8 +2341,8 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input:checked + label:hover, .formChange ul.holdMember li input:checked + label:hover { - background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right - center; + background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat + right center; background-size: 1.3rem; } .formChange > p { @@ -2471,7 +2495,8 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input + label:hover, .formChange ul.holdMember li input + label:hover { - background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center; + background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left + center; background-size: 1.3rem; } .formChange ul.chooseMember li input:checked + label, @@ -2482,8 +2507,8 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input:checked + label:hover, .formChange ul.holdMember li input:checked + label:hover { - background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right - center; + background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat + right center; background-size: 1.3rem; } .formChange > p { diff --git a/dictation_client/src/styles/app.module.scss.d.ts b/dictation_client/src/styles/app.module.scss.d.ts index c4b1492..2dde822 100644 --- a/dictation_client/src/styles/app.module.scss.d.ts +++ b/dictation_client/src/styles/app.module.scss.d.ts @@ -107,6 +107,7 @@ declare const classNames: { readonly clm0: "clm0"; readonly menuInTable: "menuInTable"; readonly isSelected: "isSelected"; + readonly odd: "odd"; readonly alignRight: "alignRight"; readonly menuAction: "menuAction"; readonly inTable: "inTable"; diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 4283e39..bb2c373 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -128,7 +128,14 @@ "authorIdIncorrectError": "Das Format der Autoren-ID ist ungültig. Als Autoren-ID können nur alphanumerische Zeichen und „_“ eingegeben werden.", "roleChangeError": "Die Benutzerrolle kann nicht geändert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.", "encryptionPasswordCorrectError": "Das Verschlüsselungskennwort entspricht nicht den Regeln.", - "alreadyLicenseDeallocatedError": "Die zugewiesene Lizenz wurde bereits storniert. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen." + "alreadyLicenseDeallocatedError": "Die zugewiesene Lizenz wurde bereits storniert. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.", + "UserDeletionLicenseActiveError": "(de)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", + "TypistDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", + "AdminUserDeletionError": "(de)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", + "TypistUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", + "AuthorUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", + "TypistUserDeletionTranscriptionistGroupError": "(de)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", + "AuthorDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" }, "label": { "title": "Benutzer", @@ -417,7 +424,7 @@ }, "message": { "selectedTypistEmptyError": "Um eine Transkriptionsgruppe zu speichern, müssen ein oder mehrere Transkriptionisten ausgewählt werden.", - "groupSaveFailedError": "Die Schreibkraftgruppe konnte nicht gespeichert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen." + "groupSaveFailedError": "Die Transkriptionistengruppe konnte nicht gespeichert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen." } }, "worktypeIdSetting": { @@ -509,6 +516,9 @@ }, "message": { "updateAccountFailedError": "Kontoinformationen konnten nicht gespeichert werden. Bitte aktualisieren Sie den Bildschirm und versuchen Sie es erneut." + }, + "text": { + "dealerManagementAnnotation": "Durch die Aktivierung der Option „Erlauben Sie dem Händler, Änderungen vorzunehmen“ erklären Sie sich damit einverstanden, dass Ihr Händler die Rechte erhält, auf Ihr ODMS Cloud-Konto zuzugreifen, um in Ihrem Namen Lizenzen zu bestellen und Benutzer zu registrieren. Ihr Händler hat keinen Zugriff auf Sprachdateien oder Dokumente, die in Ihrem ODMS Cloud-Konto gespeichert sind." } }, "deleteAccountPopup": { diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 2a4baf1..4c97388 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -128,7 +128,14 @@ "authorIdIncorrectError": "Author ID format is invalid. Only alphanumeric characters and \"_\" can be entered for Author ID.", "roleChangeError": "Unable to change the User Role. The displayed information may be outdated, so please refresh the screen to see the latest status.", "encryptionPasswordCorrectError": "Encryption password does not meet the rules.", - "alreadyLicenseDeallocatedError": "Assigned license has already been canceled. The displayed information may be outdated, so please refresh the screen to see the latest status." + "alreadyLicenseDeallocatedError": "Assigned license has already been canceled. The displayed information may be outdated, so please refresh the screen to see the latest status.", + "UserDeletionLicenseActiveError": "ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", + "TypistDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", + "AdminUserDeletionError": "ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", + "TypistUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", + "AuthorUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", + "TypistUserDeletionTranscriptionistGroupError": "ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", + "AuthorDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" }, "label": { "title": "User", @@ -417,7 +424,7 @@ }, "message": { "selectedTypistEmptyError": "One or more transcriptonist must be selected to save a transcrption group.", - "groupSaveFailedError": "Typist Group could not be saved. The displayed information may be outdated, so please refresh the screen to see the latest status." + "groupSaveFailedError": "Transcriptionist Group could not be saved. The displayed information may be outdated, so please refresh the screen to see the latest status." } }, "worktypeIdSetting": { @@ -509,6 +516,9 @@ }, "message": { "updateAccountFailedError": "Failed to save account information. Please refresh the screen and try again." + }, + "text": { + "dealerManagementAnnotation": "By enabling the \"Dealer Management\" option, you are agreeing to allow your dealer to have the rights to access your ODMS Cloud account to order licenses and register users on your behalf. Your dealer will not have access to any voice file(s) or document(s) stored in your ODMS Cloud account." } }, "deleteAccountPopup": { @@ -558,4 +568,4 @@ "close": "Close" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 43d4d44..aa43165 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -28,7 +28,7 @@ "tier1": "Admin", "tier2": "BC", "tier3": "Distribuidor", - "tier4": "Concesionario", + "tier4": "Distribuidor", "tier5": "Cliente", "notSelected": "Ninguno", "signOutButton": "cerrar sesión" @@ -62,14 +62,14 @@ "title": "Crea tu cuenta", "accountInfoTitle": "Información de Registro", "countryExplanation": "Seleccione el país donde se encuentra. Si su país no aparece en la lista, seleccione el país más cercano.", - "dealerExplanation": "Seleccione el concesionario al que le gustaría comprar la licencia.", + "dealerExplanation": "Seleccione el distribuidor al que le gustaría comprar la licencia.", "adminInfoTitle": "Registre la información del administrador principal", "passwordTerms": "Establezca una contraseña. La contraseña debe tener entre 8 y 25 caracteres y debe contener letras, números y símbolos. (Debe enumerar el símbolo compatible e indicar si se necesita una letra mayúscula)." }, "label": { "company": "Nombre de empresa", "country": "País", - "dealer": "Concesionario (Opcional)", + "dealer": "Distribuidor (Opcional)", "adminName": "Nombre del administrador", "email": "Dirección de correo electrónico", "password": "Contraseña", @@ -93,7 +93,7 @@ "label": { "company": "Nombre de empresa", "country": "País", - "dealer": "Concesionario (Opcional)", + "dealer": "Distribuidor (Opcional)", "adminName": "Nombre del administrador", "email": "Dirección de correo electrónico", "password": "Contraseña", @@ -128,7 +128,14 @@ "authorIdIncorrectError": "El formato de ID del autor no es válido. Sólo se pueden ingresar caracteres alfanuméricos y \"_\" para la ID del autor.", "roleChangeError": "No se puede cambiar la función de usuario. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente.", "encryptionPasswordCorrectError": "La contraseña de cifrado no cumple con las reglas.", - "alreadyLicenseDeallocatedError": "La licencia asignada ya ha sido cancelada. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente." + "alreadyLicenseDeallocatedError": "La licencia asignada ya ha sido cancelada. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente.", + "UserDeletionLicenseActiveError": "(es)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", + "TypistDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", + "AdminUserDeletionError": "(es)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", + "TypistUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", + "AuthorUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", + "TypistUserDeletionTranscriptionistGroupError": "(es)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", + "AuthorDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" }, "label": { "title": "Usuario", @@ -417,7 +424,7 @@ }, "message": { "selectedTypistEmptyError": "Se deben seleccionar uno o más transcriptores para guardar un grupo de transcripción.", - "groupSaveFailedError": "No se pudo guardar el grupo mecanógrafo. La información mostrada puede estar desactualizada, así que actualice la pantalla para ver el estado más reciente." + "groupSaveFailedError": "El grupo transcriptor no se pudo salvar. La información mostrada puede estar desactualizada. Así que actualice la pantalla para ver el estado más reciente." } }, "worktypeIdSetting": { @@ -496,9 +503,9 @@ "accountID": "ID de la cuenta", "yourCategory": "Tipo de cuenta", "yourCountry": "País", - "yourDealer": "Concesionario", - "selectDealer": "Seleccionar Concesionario", - "dealerManagement": "Permitir que el concesionario realice los cambios", + "yourDealer": "Distribuidor", + "selectDealer": "Seleccionar distribuidor", + "dealerManagement": "Permitir que el distribuidor realice los cambios", "administratorInformation": "Información del administrador", "primaryAdministrator": "Administrador primario", "secondaryAdministrator": "Administrador secundario", @@ -509,6 +516,9 @@ }, "message": { "updateAccountFailedError": "No se pudo guardar la información de la cuenta. Actualice la pantalla e inténtelo de nuevo." + }, + "text": { + "dealerManagementAnnotation": "Al habilitar la opción \"Permitir que el distribuidor realice los cambios\", usted acepta permitir que su distribuidor tenga derechos para acceder a su cuenta de ODMS Cloud para solicitar licencias y registrar usuarios en su nombre. Su distribuidor no tendrá acceso a ningún archivo de voz o documento almacenado en su cuenta de ODMS Cloud." } }, "deleteAccountPopup": { @@ -558,4 +568,4 @@ "close": "Cerrar" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 57c5da9..aafb91d 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -28,7 +28,7 @@ "tier1": "Admin", "tier2": "BC", "tier3": "Distributeur", - "tier4": "Concessionnaire", + "tier4": "Revendeur", "tier5": "Client", "notSelected": "Aucune", "signOutButton": "se déconnecter" @@ -62,14 +62,14 @@ "title": "Créez votre compte", "accountInfoTitle": "Information d'inscription", "countryExplanation": "Sélectionnez le pays où vous vous trouvez. Si votre pays ne figure pas dans la liste, veuillez sélectionner le pays le plus proche.", - "dealerExplanation": "Veuillez sélectionner le concessionnaire auprès duquel vous souhaitez acheter la licence.", + "dealerExplanation": "Veuillez sélectionner le revendeur auprès duquel vous souhaitez acheter la licence.", "adminInfoTitle": "Enregistrer les informations de l'administrateur principal", "passwordTerms": "Veuillez définir un mot de passe. Le mot de passe doit être composé de 8 à 25 caractères et doit contenir des lettres, des chiffres et des symboles. (Devrait lister les symboles compatibles et indiquer si une majuscule est nécessaire)." }, "label": { "company": "Nom de l'entreprise", "country": "Pays", - "dealer": "Concessionnaire (Facultatif)", + "dealer": "Revendeur (Facultatif)", "adminName": "Nom de l'administrateur", "email": "Adresse e-mail", "password": "Mot de passe", @@ -93,7 +93,7 @@ "label": { "company": "Nom de l'entreprise", "country": "Pays", - "dealer": "Concessionnaire (Facultatif)", + "dealer": "Revendeur (Facultatif)", "adminName": "Nom de l'administrateur", "email": "Adresse e-mail", "password": "Mot de passe", @@ -128,7 +128,14 @@ "authorIdIncorrectError": "Le format de l'identifiant de l'auteur n'est pas valide. Seuls les caractères alphanumériques et \"_\" peuvent être saisis pour l'ID d'auteur.", "roleChangeError": "Impossible de modifier le rôle de l'utilisateur. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.", "encryptionPasswordCorrectError": "Le mot de passe de cryptage n'est pas conforme aux règles.", - "alreadyLicenseDeallocatedError": "La licence attribuée a déjà été annulée. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut." + "alreadyLicenseDeallocatedError": "La licence attribuée a déjà été annulée. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.", + "UserDeletionLicenseActiveError": "(fr)ユーザーの削除に失敗しました。対象ユーザーのライセンス割り当てを解除してください。", + "TypistDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象Transcriptionistを外してください。", + "AdminUserDeletionError": "(fr)ユーザーの削除に失敗しました。アカウント画面で対象ユーザーをPrimary/Secondary Administratorから外してください。", + "TypistUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", + "AuthorUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", + "TypistUserDeletionTranscriptionistGroupError": "(fr)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", + "AuthorDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" }, "label": { "title": "Utilisateur", @@ -417,7 +424,7 @@ }, "message": { "selectedTypistEmptyError": "Un ou plusieurs transcripteurs doivent être sélectionnés pour enregistrer un groupe de transcription.", - "groupSaveFailedError": "Le groupe de dactylographes n'a pas pu être enregistré. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut." + "groupSaveFailedError": "Le groupe de transcriptionniste n'a pas pu être enregistré. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut." } }, "worktypeIdSetting": { @@ -496,9 +503,9 @@ "accountID": "identifiant de compte", "yourCategory": "Type de compte", "yourCountry": "Pays", - "yourDealer": "Concessionnaire", - "selectDealer": "Sélectionner le Concessionnaire", - "dealerManagement": "Autoriser le concessionnaire à modifier les paramètres", + "yourDealer": "Revendeur", + "selectDealer": "Sélectionner le revendeur", + "dealerManagement": "Autoriser le revendeur à modifier les paramètres", "administratorInformation": "Informations sur l'administrateur", "primaryAdministrator": "Administrateur principal", "secondaryAdministrator": "Administrateur secondaire", @@ -509,6 +516,9 @@ }, "message": { "updateAccountFailedError": "Échec de l'enregistrement des informations du compte. Veuillez actualiser l'écran et réessayer." + }, + "text": { + "dealerManagementAnnotation": "En activant l'option « Autoriser le revendeur à modifier les paramètres », vous acceptez que votre concessionnaire ait les droits d'accès à votre compte ODMS Cloud pour commander des licences et enregistrer des utilisateurs en votre nom. Votre revendeur n'aura accès à aucun fichier(s) vocal(s) ou document(s) stocké(s) dans votre compte ODMS Cloud." } }, "deleteAccountPopup": { @@ -558,4 +568,4 @@ "close": "Fermer" } } -} +} \ No newline at end of file From 878657ad4a749ac278c6a64c00bc7bf315bbef12 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 5 Feb 2024 06:19:24 +0000 Subject: [PATCH 15/18] =?UTF-8?q?Merged=20PR=20713:=20=E8=A1=8C=E3=83=AD?= =?UTF-8?q?=E3=83=83=E3=82=AF=E6=A8=AA=E5=B1=95=E9=96=8B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3471: 行ロック横展開3](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3471) - 以下のリポジトリのメソッドに行ロックを追加しました。 - tasks - checkout - タスク、チェックアウト情報にロックを追加して途中で変更できないようにする - checkin - タスクのチェックにロックを追加してタスクのステータスを途中で変更できないようにする - cancel - タスクのチェックにロックを追加してタスクのステータスを途中で変更できないようにする - suspend - タスクのチェックにロックを追加してタスクのステータスを途中で変更できないようにする - backup - タスクのチェックにロックを追加してタスクのステータスを途中で変更できないようにする - create - タスクのチェックにロックを追加してJobNumberのチェックで重複しないようにする - changeCheckoutPermission - タスク、チェックアウト候補のチェックにロックを追加して途中で変更されないようにする - autoRouting - 処理中にワークフロー・ワークタイプの取得にロックを追加して意図しない対象のでチェックアウト権限が作成されないようにする - ワークタイプ・ワークフローの更新/削除時にもロックを追加 こちらの資料を参考に対応しています。 [行ロックに関する影響調査](https://ndstokyo.sharepoint.com/:x:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E8%A1%8C%E3%83%AD%E3%83%83%E3%82%AF%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E5%BD%B1%E9%9F%BF%E8%AA%BF%E6%9F%BB.xlsx?d=wdd6f3d97f7b04a538095c459f8eee2eb&csf=1&web=1&e=9M43di) 対応箇所について以下にまとめました。 [Task3471](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3471?csf=1&web=1&e=wptJqD) ## レビューポイント - 対応箇所は適切でしょうか? ## UIの変更 - なし ## 動作確認状況 - ローカルで確認 --- .../db/migrations/056-add_tasks_index.sql | 27 +++++ .../tasks/tasks.repository.service.ts | 103 +++++++++++------- .../worktypes/worktypes.repository.service.ts | 1 + 3 files changed, 94 insertions(+), 37 deletions(-) create mode 100644 dictation_server/db/migrations/056-add_tasks_index.sql diff --git a/dictation_server/db/migrations/056-add_tasks_index.sql b/dictation_server/db/migrations/056-add_tasks_index.sql new file mode 100644 index 0000000..983ea67 --- /dev/null +++ b/dictation_server/db/migrations/056-add_tasks_index.sql @@ -0,0 +1,27 @@ +-- +migrate Up +ALTER TABLE `tasks` ADD INDEX `idx_tasks_audio_file_id` (audio_file_id); +ALTER TABLE `tasks` ADD INDEX `idx_tasks_status` (status); +ALTER TABLE `tasks` ADD INDEX `idx_tasks_typist_user_id` (typist_user_id); +ALTER TABLE `tasks` ADD INDEX `idx_tasks_is_job_number_enabled` (is_job_number_enabled); +ALTER TABLE `checkout_permission` ADD INDEX `idx_checkout_permission_task_id` (task_id); +ALTER TABLE `checkout_permission` ADD INDEX `idx_checkout_permission_user_group_id` (user_group_id); +ALTER TABLE `checkout_permission` ADD INDEX `idx_checkout_permission_user_id` (user_id); +ALTER TABLE `users` ADD INDEX `idx_users_role` (role); +ALTER TABLE `users` ADD INDEX `idx_users_author_id` (author_id); +ALTER TABLE `users` ADD INDEX `idx_users_deleted_at` (deleted_at); +ALTER TABLE `worktypes` ADD INDEX `idx_worktypes_custom_worktype_id` (custom_worktype_id); +ALTER TABLE `workflows` ADD INDEX `idx_workflows_account_id` (account_id); + +-- +migrate Down +ALTER TABLE `tasks` DROP INDEX `idx_tasks_audio_file_id`; +ALTER TABLE `tasks` DROP INDEX `idx_tasks_status`; +ALTER TABLE `tasks` DROP INDEX `idx_tasks_typist_user_id`; +ALTER TABLE `tasks` DROP INDEX `idx_tasks_is_job_number_enabled`; +ALTER TABLE `checkout_permission` DROP INDEX `idx_checkout_permission_task_id`; +ALTER TABLE `checkout_permission` DROP INDEX `idx_checkout_permission_user_group_id`; +ALTER TABLE `checkout_permission` DROP INDEX `idx_checkout_permission_user_id`; +ALTER TABLE `users` DROP INDEX `idx_users_role`; +ALTER TABLE `users` DROP INDEX `idx_users_author_id`; +ALTER TABLE `users` DROP INDEX `idx_users_deleted_at`; +ALTER TABLE `worktypes` DROP INDEX `idx_worktypes_custom_worktype_id`; +ALTER TABLE `workflows` DROP INDEX `idx_workflows_account_id`; \ No newline at end of file diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 824a85c..a95a8ab 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -189,6 +189,7 @@ export class TasksRepositoryService { audio_file_id: audio_file_id, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!task) { throw new TasksNotFoundError( @@ -204,6 +205,7 @@ export class TasksRepositoryService { typist_user_id: user_id, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (tasks.length > 0) { @@ -242,6 +244,7 @@ export class TasksRepositoryService { }, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // ユーザーの所属するすべてのグループIDを列挙 const groupIds = groups.map((member) => member.user_group_id); @@ -264,6 +267,7 @@ export class TasksRepositoryService { }, ], comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); //チェックアウト権限がなければエラー @@ -334,6 +338,7 @@ export class TasksRepositoryService { audio_file_id: audio_file_id, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!task) { throw new TasksNotFoundError( @@ -387,6 +392,7 @@ export class TasksRepositoryService { audio_file_id: audio_file_id, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!task) { throw new TasksNotFoundError( @@ -462,6 +468,7 @@ export class TasksRepositoryService { audio_file_id: audio_file_id, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!task) { throw new TasksNotFoundError( @@ -513,6 +520,7 @@ export class TasksRepositoryService { audio_file_id: audio_file_id, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!task) { throw new TasksNotFoundError( @@ -896,6 +904,7 @@ export class TasksRepositoryService { where: { account_id: account_id, is_job_number_enabled: true }, order: { created_at: 'DESC', job_number: 'DESC' }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); let newJobNumber = '00000001'; @@ -958,30 +967,6 @@ export class TasksRepositoryService { assignees: Assignee[], ): Promise { await this.dataSource.transaction(async (entityManager) => { - // UserGroupの取得/存在確認 - const userGroupIds = assignees - .filter((x) => x.typistGroupId !== undefined) - .map((y) => { - return y.typistGroupId; - }); - const groupRepo = entityManager.getRepository(UserGroup); - const groupRecords = await groupRepo.find({ - where: { - id: In(userGroupIds), - account_id: account_id, - deleted_at: IsNull(), - }, - comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, - }); - // idはユニークであるため取得件数の一致でグループの存在を確認 - if (userGroupIds.length !== groupRecords.length) { - throw new TypistUserGroupNotFoundError( - `Group not exists Error. reqUserGroupId:${userGroupIds}; resUserGroupId:${groupRecords.map( - (x) => x.id, - )}`, - ); - } - // Userの取得/存在確認 const typistUserIds = assignees .filter((x) => x.typistUserId !== undefined) @@ -1009,6 +994,31 @@ export class TasksRepositoryService { ); } + // UserGroupの取得/存在確認 + const userGroupIds = assignees + .filter((x) => x.typistGroupId !== undefined) + .map((y) => { + return y.typistGroupId; + }); + const groupRepo = entityManager.getRepository(UserGroup); + const groupRecords = await groupRepo.find({ + where: { + id: In(userGroupIds), + account_id: account_id, + deleted_at: IsNull(), + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + // idはユニークであるため取得件数の一致でグループの存在を確認 + if (userGroupIds.length !== groupRecords.length) { + throw new TypistUserGroupNotFoundError( + `Group not exists Error. reqUserGroupId:${userGroupIds}; resUserGroupId:${groupRecords.map( + (x) => x.id, + )}`, + ); + } + // 引数audioFileIdを使ってTaskレコードを特定し、そのステータスを取得/存在確認 const taskRepo = entityManager.getRepository(Task); @@ -1024,6 +1034,7 @@ export class TasksRepositoryService { }, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); //タスクが存在しない or ステータスがUploadedでなければエラー if (!taskRecord) { @@ -1187,39 +1198,51 @@ export class TasksRepositoryService { return await this.dataSource.transaction(async (entityManager) => { // 音声ファイルを取得 const audioFileRepo = entityManager.getRepository(AudioFile); - const audioFile = await audioFileRepo.findOne({ - relations: { - task: true, - }, + const audio = await audioFileRepo.findOne({ where: { id: audioFileId, account_id: accountId, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); - if (!audioFile) { + if (!audio) { throw new Error( `audio file not found. audio_file_id:${audioFileId}, accountId:${accountId}`, ); } - const { task } = audioFile; - - if (!task) { - throw new Error( - `task not found. audio_file_id:${audioFileId}, accountId:${accountId}`, - ); - } // authorIdをもとにユーザーを取得 const userRepo = entityManager.getRepository(User); const authorUser = await userRepo.findOne({ where: { - author_id: audioFile.author_id, + author_id: audio.author_id, account_id: accountId, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); + // TaskとFileを取得 + const taskRepo = entityManager.getRepository(Task); + const task = await taskRepo.findOne({ + relations: { + file: true, + }, + where: { + account_id: accountId, + audio_file_id: audioFileId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + const audioFile = task?.file; + if (!audioFile) { + throw new Error( + `audio file not found. audio_file_id:${audioFileId}, accountId:${accountId}`, + ); + } + // 音声ファイル上のworktypeIdをもとにworktypeを取得 const worktypeRepo = entityManager.getRepository(Worktype); const worktypeRecord = await worktypeRepo.findOne({ @@ -1228,6 +1251,7 @@ export class TasksRepositoryService { account_id: accountId, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // 音声ファイル上のworktypeIdが設定されているが、一致するworktypeが存在しない場合はエラーを出して終了 @@ -1249,6 +1273,7 @@ export class TasksRepositoryService { worktype_id: worktypeRecord?.id ?? IsNull(), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // Workflow(ルーティングルール)があればタスクのチェックアウト権限を設定する @@ -1276,6 +1301,7 @@ export class TasksRepositoryService { account_id: accountId, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!myAuthorUser) { throw new Error( @@ -1292,6 +1318,7 @@ export class TasksRepositoryService { worktype_id: worktypeRecord?.id ?? IsNull(), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // API実行者のAuthorIdと音声ファイルのWorktypeをもとにルーティングルールを取得できない場合はエラーを出して終了 @@ -1358,6 +1385,7 @@ export class TasksRepositoryService { const typistUsers = await userRepo.find({ where: { account_id: accountId, id: In(typistIds) }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (typistUsers.length !== typistIds.length) { throw new Error(`typist not found. ids: ${typistIds}`); @@ -1371,6 +1399,7 @@ export class TasksRepositoryService { const typistGroups = await userGroupRepo.find({ where: { account_id: accountId, id: In(groupIds) }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (typistGroups.length !== groupIds.length) { throw new Error(`typist group not found. ids: ${groupIds}`); diff --git a/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts b/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts index 07e1704..8ca02c9 100644 --- a/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts +++ b/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts @@ -220,6 +220,7 @@ export class WorktypesRepositoryService { const worktype = await worktypeRepo.findOne({ where: { account_id: accountId, id: id }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // ワークタイプが存在しない場合はエラー if (!worktype) { From 48d2e625db77e2f462de7efd204dc1ee47c33e9f Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Mon, 5 Feb 2024 07:08:00 +0000 Subject: [PATCH 16/18] =?UTF-8?q?Merged=20PR=20730:=20[FB=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C]=E3=82=BF=E3=82=B9=E3=82=AF=E4=B8=80=E8=A6=A7?= =?UTF-8?q?=E7=94=BB=E9=9D=A2=E3=81=AE=E3=83=87=E3=82=B6=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3614: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3614) - デザイン修正 - ソートが昇順の時は▲ - ソートが降順の時は▼ - Priorityが「High」の時にその行を赤くする ## レビューポイント - 特になし ## UIの変更 - https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3614?csf=1&web=1&e=fm3MFs ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- .../src/pages/DictationPage/index.tsx | 8 +++++- dictation_client/src/styles/app.module.scss | 26 +++++++++---------- .../src/styles/app.module.scss.d.ts | 2 +- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx index cbeed07..1d3abcc 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -1116,7 +1116,13 @@ const DictationPage: React.FC = (): JSX.Element => { {(isChangeTranscriptionistPopupOpen || !isLoading) && tasks.length !== 0 && tasks.map((x) => ( - +
  • diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index 52f8553..c075fdc 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -1343,23 +1343,23 @@ _:-ms-lang(x)::-ms-backdrop, .tableHeader th .hasSort:hover::before { opacity: 1; } -.tableHeader th .hasSort.isActiveAz::before { - opacity: 1; -} -.tableHeader th .hasSort.isActiveAz:hover::before { - border-top: none; - border-right: 0.35rem transparent solid; - border-bottom: 0.4rem #ffffff solid; - border-left: 0.35rem transparent solid; -} .tableHeader th .hasSort.isActiveZa::before { - border-top: none; - border-right: 0.35rem transparent solid; - border-bottom: 0.4rem #ffffff solid; - border-left: 0.35rem transparent solid; opacity: 1; } .tableHeader th .hasSort.isActiveZa:hover::before { + border-top: none; + border-right: 0.35rem transparent solid; + border-bottom: 0.4rem #ffffff solid; + border-left: 0.35rem transparent solid; +} +.tableHeader th .hasSort.isActiveAz::before { + border-top: none; + border-right: 0.35rem transparent solid; + border-bottom: 0.4rem #ffffff solid; + border-left: 0.35rem transparent solid; + opacity: 1; +} +.tableHeader th .hasSort.isActiveAz:hover::before { border-top: 0.4rem #ffffff solid; border-right: 0.35rem transparent solid; border-bottom: none; diff --git a/dictation_client/src/styles/app.module.scss.d.ts b/dictation_client/src/styles/app.module.scss.d.ts index 2dde822..68fcbca 100644 --- a/dictation_client/src/styles/app.module.scss.d.ts +++ b/dictation_client/src/styles/app.module.scss.d.ts @@ -89,8 +89,8 @@ declare const classNames: { readonly snackbarIcon: "snackbarIcon"; readonly snackbarIconClose: "snackbarIconClose"; readonly hasSort: "hasSort"; - readonly isActiveAz: "isActiveAz"; readonly isActiveZa: "isActiveZa"; + readonly isActiveAz: "isActiveAz"; readonly noLine: "noLine"; readonly home: "home"; readonly pgHome: "pgHome"; From 84b0da1f956badc42b54618349611a5b5451df1d Mon Sep 17 00:00:00 2001 From: "saito.k" Date: Mon, 5 Feb 2024 11:46:29 +0000 Subject: [PATCH 17/18] =?UTF-8?q?Merged=20PR=20723:=20[FB=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C]=E3=82=BF=E3=82=A4=E3=83=94=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=B0=E3=83=AB=E3=83=BC=E3=83=97=E9=87=8D=E8=A4=87=E6=99=82?= =?UTF-8?q?=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=A8=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3613: 対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3613) - タイピストグループ名が重複した際のエラーを追加 - タイピストグループ追加API - タイピストグループ更新API - タイピストグループ設定画面に表示するエラーメッセージを追加 ## レビューポイント - 行ロックするべきかどうか - ギリギリのタイミングで同名のタイピストグループが作成される場合は防げないのでDBでユニーク制約を設定する? - insertにロックはかけられないから ## UIの変更 - https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3613?csf=1&web=1&e=i8cN2g ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば --- dictation_client/src/common/errors/code.ts | 2 ++ .../workflow/typistGroup/operations.ts | 31 +++++++++++----- dictation_client/src/translation/de.json | 5 ++- dictation_client/src/translation/en.json | 7 ++-- dictation_client/src/translation/es.json | 7 ++-- dictation_client/src/translation/fr.json | 7 ++-- .../058-add_user_group_unique_constraint.sql | 6 ++++ dictation_server/src/common/error/code.ts | 1 + dictation_server/src/common/error/message.ts | 1 + .../src/features/accounts/accounts.service.ts | 13 +++++++ .../repositories/user_groups/errors/types.ts | 7 ++++ .../user_groups.repository.service.ts | 36 +++++++++++++++++-- 12 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 dictation_server/db/migrations/058-add_user_group_unique_constraint.sql diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index da0d9b9..50d91db 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -54,6 +54,8 @@ export const errorCodes = [ "E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合) "E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合) "E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) + "E010908", // タイピストグループ不在エラー + "E010909", // タイピストグループ名重複エラー "E011001", // ワークタイプ重複エラー "E011002", // ワークタイプ登録上限超過エラー "E011003", // ワークタイプ不在エラー diff --git a/dictation_client/src/features/workflow/typistGroup/operations.ts b/dictation_client/src/features/workflow/typistGroup/operations.ts index a98ff1b..70594c6 100644 --- a/dictation_client/src/features/workflow/typistGroup/operations.ts +++ b/dictation_client/src/features/workflow/typistGroup/operations.ts @@ -122,11 +122,17 @@ export const createTypistGroupAsync = createAsyncThunk< } catch (e) { // e ⇒ errorObjectに変換" const error = createErrorObject(e); - - const message = - error.statusCode === 400 - ? getTranslationID("typistGroupSetting.message.groupSaveFailedError") - : getTranslationID("common.message.internalServerError"); + let message = getTranslationID("common.message.internalServerError"); + if (error.code === "E010204") { + message = getTranslationID( + "typistGroupSetting.message.groupSaveFailedError" + ); + } + if (error.code === "E010909") { + message = getTranslationID( + "typistGroupSetting.message.GroupNameAlreadyExistError" + ); + } thunkApi.dispatch( openSnackbar({ @@ -242,10 +248,17 @@ export const updateTypistGroupAsync = createAsyncThunk< // e ⇒ errorObjectに変換" const error = createErrorObject(e); - const message = - error.statusCode === 400 - ? getTranslationID("typistGroupSetting.message.groupSaveFailedError") - : getTranslationID("common.message.internalServerError"); + let message = getTranslationID("common.message.internalServerError"); + if (error.code === "E010204" || error.code === "E010908") { + message = getTranslationID( + "typistGroupSetting.message.groupSaveFailedError" + ); + } + if (error.code === "E010909") { + message = getTranslationID( + "typistGroupSetting.message.GroupNameAlreadyExistError" + ); + } thunkApi.dispatch( openSnackbar({ diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index bb2c373..bff0bba 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -424,7 +424,10 @@ }, "message": { "selectedTypistEmptyError": "Um eine Transkriptionsgruppe zu speichern, müssen ein oder mehrere Transkriptionisten ausgewählt werden.", - "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." + "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": { diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 4c97388..e0f1800 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -424,7 +424,10 @@ }, "message": { "selectedTypistEmptyError": "One or more transcriptonist must be selected to save a transcrption group.", - "groupSaveFailedError": "Transcriptionist 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": { @@ -568,4 +571,4 @@ "close": "Close" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index aa43165..564958a 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -424,7 +424,10 @@ }, "message": { "selectedTypistEmptyError": "Se deben seleccionar uno o más transcriptores para guardar un grupo de transcripción.", - "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." + "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": { @@ -568,4 +571,4 @@ "close": "Cerrar" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index aafb91d..9cbdcd2 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -424,7 +424,10 @@ }, "message": { "selectedTypistEmptyError": "Un ou plusieurs transcripteurs doivent être sélectionnés pour enregistrer un groupe de transcription.", - "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." + "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": { @@ -568,4 +571,4 @@ "close": "Fermer" } } -} \ No newline at end of file +} diff --git a/dictation_server/db/migrations/058-add_user_group_unique_constraint.sql b/dictation_server/db/migrations/058-add_user_group_unique_constraint.sql new file mode 100644 index 0000000..c9cb2d6 --- /dev/null +++ b/dictation_server/db/migrations/058-add_user_group_unique_constraint.sql @@ -0,0 +1,6 @@ +-- +migrate Up +ALTER TABLE `user_group` ADD UNIQUE `unique_index_account_id_name` (`account_id`, `name`); + + +-- +migrate Down +ALTER TABLE `user_group` DROP INDEX `unique_index_account_id_name`; diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 8f9dc2f..3c488da 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -59,6 +59,7 @@ export const ErrorCodes = [ 'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) 'E010812', // ライセンス未割当エラー 'E010908', // タイピストグループ不在エラー + 'E010909', // タイピストグループ名重複エラー 'E011001', // ワークタイプ重複エラー 'E011002', // ワークタイプ登録上限超過エラー 'E011003', // ワークタイプ不在エラー diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 0a27cc5..9383694 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -48,6 +48,7 @@ export const errors: Errors = { E010811: 'Already license allocated Error', E010812: 'License not allocated Error', E010908: 'Typist Group not exist Error', + E010909: 'Typist Group name already exist Error', E011001: 'This WorkTypeID already used Error', E011002: 'WorkTypeID create limit exceeded Error', E011003: 'WorkTypeID not found Error', diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 50273e6..8b6e271 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -60,6 +60,7 @@ import { } from '../../repositories/licenses/errors/types'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { + TypistGroupNameAlreadyExistError, TypistGroupNotExistError, TypistIdInvalidError, } from '../../repositories/user_groups/errors/types'; @@ -1241,6 +1242,12 @@ export class AccountsService { makeErrorResponse('E010204'), HttpStatus.BAD_REQUEST, ); + // 同名のタイピストグループが存在する場合は400エラーを返す + case TypistGroupNameAlreadyExistError: + throw new HttpException( + makeErrorResponse('E010909'), + HttpStatus.BAD_REQUEST, + ); default: throw new HttpException( makeErrorResponse('E009999'), @@ -1315,6 +1322,12 @@ export class AccountsService { makeErrorResponse('E010908'), HttpStatus.BAD_REQUEST, ); + // 同名のタイピストグループが存在する場合は400エラーを返す + case TypistGroupNameAlreadyExistError: + throw new HttpException( + makeErrorResponse('E010909'), + HttpStatus.BAD_REQUEST, + ); default: throw new HttpException( makeErrorResponse('E009999'), diff --git a/dictation_server/src/repositories/user_groups/errors/types.ts b/dictation_server/src/repositories/user_groups/errors/types.ts index 57aabbb..4a215a7 100644 --- a/dictation_server/src/repositories/user_groups/errors/types.ts +++ b/dictation_server/src/repositories/user_groups/errors/types.ts @@ -12,3 +12,10 @@ export class TypistIdInvalidError extends Error { this.name = 'TypistIdInvalidError'; } } +// 同名のタイピストグループが存在する場合のエラー +export class TypistGroupNameAlreadyExistError extends Error { + constructor(message: string) { + super(message); + this.name = 'TypistGroupNameAlreadyExistError'; + } +} \ No newline at end of file diff --git a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts index 050d3e1..a900569 100644 --- a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts +++ b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts @@ -1,9 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { DataSource, In, IsNull } from 'typeorm'; +import { DataSource, In, IsNull, Not } from 'typeorm'; import { UserGroup } from './entity/user_group.entity'; import { UserGroupMember } from './entity/user_group_member.entity'; import { User } from '../users/entity/user.entity'; -import { TypistGroupNotExistError, TypistIdInvalidError } from './errors/types'; +import { + TypistGroupNameAlreadyExistError, + TypistGroupNotExistError, + TypistIdInvalidError, +} from './errors/types'; import { USER_ROLES } from '../../constants'; import { insertEntities, @@ -132,6 +136,19 @@ export class UserGroupsRepositoryService { )}`, ); } + // 同名のタイピストグループが存在するか確認する + const sameNameTypistGroup = await userGroupRepo.findOne({ + where: { + name, + account_id: accountId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + if (sameNameTypistGroup) { + throw new TypistGroupNameAlreadyExistError( + `TypistGroup already exists Error. accountId: ${accountId}; name: ${name}`, + ); + } // userGroupをDBに保存する const userGroup = await insertEntity( UserGroup, @@ -200,6 +217,21 @@ export class UserGroupsRepositoryService { ); } + // 同名のタイピストグループが存在するか確認する + const sameNameTypistGroup = await userGroupRepo.findOne({ + where: { + id: Not(typistGroupId), + name: typistGroupName, + account_id: accountId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + if (sameNameTypistGroup) { + throw new TypistGroupNameAlreadyExistError( + `TypistGroup already exists Error. accountId: ${accountId}; name: ${typistGroupName}`, + ); + } + // GroupIdが自アカウント内に存在するか確認する const typistGroup = await userGroupRepo.findOne({ where: { From a1b7505035d3f2a4754bab3fcb689c42e7ff2b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=AF=E6=9C=AC=20=E9=96=8B?= Date: Tue, 6 Feb 2024 05:03:45 +0000 Subject: [PATCH 18/18] =?UTF-8?q?Merged=20PR=20732:=20[=E6=94=B9=E5=96=84]?= =?UTF-8?q?=E8=AA=8D=E8=A8=BC=E7=94=A8URL=E3=81=AB=E3=81=A4=E3=81=84?= =?UTF-8?q?=E3=81=A6=E3=80=81=E3=83=89=E3=83=A1=E3=82=A4=E3=83=B3=E5=90=8D?= =?UTF-8?q?=E3=81=AE=E6=9C=AB=E5=B0=BE=E3=81=AB/=E3=81=8C=E5=BF=85?= =?UTF-8?q?=E8=A6=81=E3=81=A8=E3=81=AA=E3=82=8B=E3=81=93=E3=81=A8=E3=81=B8?= =?UTF-8?q?=E3=81=AE=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3625: [改善]認証用URLについて、ドメイン名の末尾に/が必要となることへの対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3625) - URLクラスとpathクラスを用いてURLを構築するよう修正 - 送信するメールに関わるテストを追加 ## レビューポイント - 修正内容は妥当であるか - 漏れていそうなURL系の処理はないか - 工数面を考慮したコスト対効果の観点から、メール送信を行うテスト全てに送信メール内容のチェックを行うテストコードは入れなかったが許容可能か ## 動作確認状況 - npm run testは通過 - `.env.test` の `APP_DOMAIN` の末尾 `/` を付けて通過 & 消して通過 するかを確認 - **一応追試をお願いしたいです** --- dictation_server/src/common/test/overrides.ts | 3 +- .../accounts/accounts.service.spec.ts | 115 +++++++++++++++++- .../licenses/licenses.service.spec.ts | 72 ++++++++++- .../src/features/users/users.service.spec.ts | 49 +++++++- .../src/gateways/sendgrid/sendgrid.service.ts | 40 +++--- 5 files changed, 250 insertions(+), 29 deletions(-) diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts index 146d00a..ffaf05d 100644 --- a/dictation_server/src/common/test/overrides.ts +++ b/dictation_server/src/common/test/overrides.ts @@ -82,7 +82,8 @@ export const overrideSendgridService = ( overrides: { sendMail?: ( context: Context, - to: string, + to: string[], + cc: string[], from: string, subject: string, text: string, diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 4bfd687..d661635 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -134,7 +134,26 @@ describe('createAccount', () => { }, }); - overrideSendgridService(service, {}); + let _subject: string = ""; + let _url: string | undefined = ""; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + const urlPattern = /https?:\/\/[^\s]+/g; + const urls = text.match(urlPattern); + const url = urls?.pop(); + + _subject = subject; + _url = url; + }, + }); overrideBlobstorageService(service, { createContainer: async () => { return; @@ -175,6 +194,10 @@ describe('createAccount', () => { expect(user?.accepted_dpa_version).toBe(acceptedDpaVersion); expect(user?.account_id).toBe(accountId); expect(user?.role).toBe(role); + + // 想定通りのメールが送られているか確認 + expect(_subject).toBe('User Registration Notification [U-102]'); + expect(_url?.startsWith('http://localhost:8081/mail-confirm?verify=')).toBeTruthy(); }); it('アカウントを作成がAzure AD B2Cへの通信失敗によって失敗すると500エラーが発生する', async () => { @@ -5704,9 +5727,39 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + let _subject: string = ""; + let _url: string | undefined = ""; overrideSendgridService(service, { - sendMail: async () => { - return; + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + const urlPattern = /https?:\/\/[^\s]+/g; + const urls = text.match(urlPattern); + const url = urls?.pop(); + + _subject = subject; + _url = url; + }, + }); + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'TEMP' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; }, }); @@ -5733,6 +5786,9 @@ describe('アカウント情報更新', () => { expect(account?.delegation_permission).toBe(true); expect(account?.primary_admin_user_id).toBe(tier5Accounts.admin.id); expect(account?.secondary_admin_user_id).toBe(null); + // 想定通りのメールが送られているか確認 + expect(_subject).toBe('Account Edit Notification [U-112]'); + expect(_url).toBe('http://localhost:8081/'); }); it('アカウント情報を更新する(第五階層以外が実行)', async () => { if (!source) fail(); @@ -6364,7 +6420,27 @@ describe('deleteAccountAndData', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); - overrideSendgridService(service, {}); + let _subject: string = ''; + let _url: string | undefined = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + const urlPattern = /https?:\/\/[^\s]+/g; + const urls = text.match(urlPattern); + const url = urls?.pop(); + + _subject = subject; + _url = url; + }, + }); + // 第一~第四階層のアカウント作成 const { tier1Accounts: tier1Accounts, @@ -6485,10 +6561,36 @@ describe('deleteAccountAndData', () => { licensesB[0].id, ); - // ADB2Cユーザーの削除成功 overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'TEMP' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => { + return externalIds.map((x) => ({ + displayName: 'admin', + id: x, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: `mail+${x}@example.com`, + }, + ], + })); + }, deleteUsers: jest.fn(), }); + // blobstorageコンテナの削除成功 overrideBlobstorageService(service, { deleteContainer: jest.fn(), @@ -6559,6 +6661,9 @@ describe('deleteAccountAndData', () => { const LicenseAllocationHistoryArchive = await getLicenseAllocationHistoryArchive(source); expect(LicenseAllocationHistoryArchive.length).toBe(1); + + expect(_subject).toBe('Account Deleted Notification [U-111]'); + expect(_url).toBe('http://localhost:8081/'); }); it('アカウントの削除に失敗した場合はエラーを返す', async () => { if (!source) fail(); diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts index 22be77f..cdc56b2 100644 --- a/dictation_server/src/features/licenses/licenses.service.spec.ts +++ b/dictation_server/src/features/licenses/licenses.service.spec.ts @@ -17,15 +17,15 @@ import { selectOrderLicense, } from './test/utility'; import { UsersService } from '../users/users.service'; -import { makeContext } from '../../common/log'; -import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants'; +import { Context, makeContext } from '../../common/log'; +import { ADB2C_SIGN_IN_TYPE, LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants'; import { makeHierarchicalAccounts, makeTestSimpleAccount, makeTestUser, } from '../../common/test/utility'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; -import { overrideSendgridService } from '../../common/test/overrides'; +import { overrideAdB2cService, overrideSendgridService } from '../../common/test/overrides'; import { truncateAllTable } from '../../common/test/init'; describe('ライセンス注文', () => { @@ -672,7 +672,18 @@ describe('ライセンス割り当て', () => { const module = await makeTestingModule(source); if (!module) fail(); - const { id: accountId } = await makeTestSimpleAccount(source); + const { id: dealerId } = await makeTestSimpleAccount(source, { company_name: "DEALER_COMPANY", tier: 4 }); + const { id: dealerAdminId } = await makeTestUser(source, { + account_id: dealerId, + external_id: 'userId_admin', + role: 'admin', + author_id: undefined, + }); + + const { id: accountId } = await makeTestSimpleAccount(source, { + parent_account_id: dealerId, + tier: 5 + }); const { id: userId } = await makeTestUser(source, { account_id: accountId, external_id: 'userId', @@ -701,7 +712,55 @@ describe('ライセンス割り当て', () => { ); const service = module.get(UsersService); - overrideSendgridService(service, {}); + let _subject: string = ''; + let _url: string | undefined = ''; + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'TEMP' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => { + return externalIds.map((x) => ({ + displayName: 'admin', + id: x, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: `mail+${x}@example.com`, + }, + ], + })); + } + }); + + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + const urlPattern = /https?:\/\/[^\s]+/g; + const urls = text.match(urlPattern); + const url = urls?.pop(); + + _subject = subject; + _url = url; + }, + }); const expiry_date = new NewAllocatedLicenseExpirationDate(); @@ -735,6 +794,9 @@ describe('ライセンス割り当て', () => { expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe( accountId, ); + + expect(_subject).toBe('License Assigned Notification [U-108]'); + expect(_url).toBe('http://localhost:8081/'); }); it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => { diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index cb27055..8e3cbf5 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -108,7 +108,26 @@ describe('UsersService.confirmUser', () => { }); const service = module.get(UsersService); - overrideSendgridService(service, {}); + let _subject: string = ''; + let _url: string | undefined = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + const urlPattern = /https?:\/\/[^\s]+/g; + const urls = text.match(urlPattern); + const url = urls?.pop(); + + _subject = subject; + _url = url; + }, + }); // account id:1, user id: 2のトークン const token = @@ -149,6 +168,8 @@ describe('UsersService.confirmUser', () => { delete_order_id: null, user: null, }); + expect(_subject).toBe('Account Registered Notification [U-101]'); + expect(_url).toBe('http://localhost:8081/'); }, 600000); it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => { @@ -506,7 +527,26 @@ describe('UsersService.createUser', () => { }; }, }); - overrideSendgridService(service, {}); + let _subject: string = ''; + let _url: string | undefined = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + const urlPattern = /https?:\/\/[^\s]+/g; + const urls = text.match(urlPattern); + const url = urls?.pop(); + + _subject = subject; + _url = url; + }, + }); expect( await service.createUser( @@ -536,6 +576,11 @@ describe('UsersService.createUser', () => { // 他にユーザーが登録されていないことを確認 const users = await getUsers(source); expect(users.length).toEqual(2); + + expect(_subject).toBe('User Registration Notification [U-114]'); + expect( + _url?.startsWith('http://localhost:8081/mail-confirm/user?verify='), + ).toBeTruthy(); }); it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化あり)', async () => { diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index 6e2fb15..985c7b1 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -21,6 +21,7 @@ import { VERIFY_LINK, TEMPORARY_PASSWORD, } from '../../templates/constants'; +import { URL } from 'node:url'; @Injectable() export class SendGridService { @@ -204,12 +205,13 @@ export class SendGridService { ); try { const subject = 'Account Registered Notification [U-101]'; + const url = new URL(this.appDomain).href; const html = this.templateU101Html .replaceAll(CUSTOMER_NAME, customerAccountName) - .replaceAll(TOP_URL, this.appDomain); + .replaceAll(TOP_URL, url); const text = this.templateU101Text .replaceAll(CUSTOMER_NAME, customerAccountName) - .replaceAll(TOP_URL, this.appDomain); + .replaceAll(TOP_URL, url); await this.sendMail( context, @@ -255,8 +257,9 @@ export class SendGridService { this.emailConfirmLifetime, privateKey, ); - const path = 'mail-confirm/'; - const verifyUrl = `${this.appDomain}${path}?verify=${token}`; + const paths = path.join('mail-confirm'); + const url = new URL(paths, this.appDomain).href; + const verifyUrl = `${url}?verify=${token}`; const subject = 'User Registration Notification [U-102]'; const html = this.templateU102Html.replaceAll(VERIFY_LINK, verifyUrl); @@ -466,6 +469,7 @@ export class SendGridService { ); try { const subject = 'License Assigned Notification [U-108]'; + const url = new URL(this.appDomain).href; // メールの本文を作成する const html = this.templateU108Html @@ -473,13 +477,13 @@ export class SendGridService { .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(USER_NAME, userName) .replaceAll(USER_EMAIL, userMail) - .replaceAll(TOP_URL, this.appDomain); + .replaceAll(TOP_URL, url); const text = this.templateU108Text .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(USER_NAME, userName) .replaceAll(USER_EMAIL, userMail) - .replaceAll(TOP_URL, this.appDomain); + .replaceAll(TOP_URL, url); const ccAddress = customerAdminMails.includes(userMail) ? [] : [userMail]; @@ -574,17 +578,18 @@ export class SendGridService { ); try { const subject = 'Account Deleted Notification [U-111]'; + const url = new URL(this.appDomain).href; // メールの本文を作成する const html = this.templateU111Html .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) - .replaceAll(TOP_URL, this.appDomain); + .replaceAll(TOP_URL, url); const text = this.templateU111Text .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) - .replaceAll(TOP_URL, this.appDomain); + .replaceAll(TOP_URL, url); // メールを送信する await this.sendMail( @@ -627,6 +632,7 @@ export class SendGridService { let html: string; let text: string; + const url = new URL(this.appDomain).href; // 親アカウントがない場合は別のテンプレートを使用する if (dealerAccountName === null) { @@ -634,22 +640,22 @@ export class SendGridService { html = this.templateU112NoParentHtml .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) - .replaceAll(TOP_URL, this.appDomain); + .replaceAll(TOP_URL, url); text = this.templateU112NoParentText .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) - .replaceAll(TOP_URL, this.appDomain); + .replaceAll(TOP_URL, url); } else { html = this.templateU112Html .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) - .replaceAll(TOP_URL, this.appDomain); + .replaceAll(TOP_URL, url); text = this.templateU112Text .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) - .replaceAll(TOP_URL, this.appDomain); + .replaceAll(TOP_URL, url); } // メールを送信する @@ -745,18 +751,20 @@ export class SendGridService { this.emailConfirmLifetime, privateKey, ); - const path = 'mail-confirm/user/'; - const verifyLink = `${this.appDomain}${path}?verify=${token}`; + + const paths = path.join('mail-confirm', '/user'); + const url = new URL(paths, this.appDomain); + const verifyUrl = `${url}?verify=${token}`; const subject = 'User Registration Notification [U-114]'; // メールの本文を作成する const html = this.templateU114Html .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) - .replaceAll(VERIFY_LINK, verifyLink); + .replaceAll(VERIFY_LINK, verifyUrl); const text = this.templateU114Text .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) - .replaceAll(VERIFY_LINK, verifyLink); + .replaceAll(VERIFY_LINK, verifyUrl); // メールを送信する await this.sendMail(