diff --git a/db/init/init_accounts_auto_increment.sql b/db/init/init_accounts_auto_increment.sql new file mode 100644 index 0000000..7d0aa25 --- /dev/null +++ b/db/init/init_accounts_auto_increment.sql @@ -0,0 +1,4 @@ +-- [OMDS_IS-231] アカウントIDの開始番号調整 | 課題の表示 | Backlog 対応 +-- IDからアカウント数が推測されるため、ユーザ指定の任意値を最初の番号とする +-- 一度しか実行しないため、migrate fileではなくDBの初期値として扱う。移行時の実行を想定 +ALTER TABLE accounts AUTO_INCREMENT = 853211; diff --git a/dictation_client/src/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/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx index 7f4934f..bf0f407 100644 --- a/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx @@ -85,7 +85,7 @@ export const AddWorktypeIdPopup: React.FC = ( { diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx index 248005b..a3d4fee 100644 --- a/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx @@ -84,7 +84,7 @@ export const EditWorktypeIdPopup: React.FC = ( { diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 01a37b9..885929b 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 c37d4ad..2f2c53c 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 bf3d0f7..8bf9b38 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 4c79233..3ad9880 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/.env.local.example b/dictation_server/.env.local.example index c59d570..8c03436 100644 --- a/dictation_server/.env.local.example +++ b/dictation_server/.env.local.example @@ -16,7 +16,7 @@ MAIL_FROM=xxxxx@xxxxx.xxxx NOTIFICATION_HUB_NAME=ntf-odms-dev NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX APP_DOMAIN=http://localhost:8081/ -STORAGE_TOKEN_EXPIRE_TIME=30 +STORAGE_TOKEN_EXPIRE_TIME=2 STORAGE_ACCOUNT_NAME_US=saodmsusdev STORAGE_ACCOUNT_NAME_AU=saodmsaudev STORAGE_ACCOUNT_NAME_EU=saodmseudev @@ -26,10 +26,10 @@ STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA -ACCESS_TOKEN_LIFETIME_WEB=7200000 -REFRESH_TOKEN_LIFETIME_WEB=86400000 -REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000 -EMAIL_CONFIRM_LIFETIME=86400000 +ACCESS_TOKEN_LIFETIME_WEB=7200 +REFRESH_TOKEN_LIFETIME_WEB=86400 +REFRESH_TOKEN_LIFETIME_DEFAULT=2592000 +EMAIL_CONFIRM_LIFETIME=86400 REDIS_HOST=redis-cache REDIS_PORT=6379 REDIS_PASSWORD=omdsredispass diff --git a/dictation_server/db/migrations/053-add_accounts_users_templates_index.sql b/dictation_server/db/migrations/053-add_accounts_users_templates_index.sql new file mode 100644 index 0000000..5b4a4a6 --- /dev/null +++ b/dictation_server/db/migrations/053-add_accounts_users_templates_index.sql @@ -0,0 +1,19 @@ +-- +migrate Up +ALTER TABLE `accounts` ADD INDEX `idx_accounts_tier` (tier); +ALTER TABLE `accounts` ADD INDEX `idx_accounts_parent_account_id` (parent_account_id); +ALTER TABLE `users` ADD INDEX `idx_users_external_id` (external_id); +ALTER TABLE `users` ADD INDEX `idx_users_email_verified` (email_verified); +ALTER TABLE `licenses` ADD INDEX `idx_licenses_order_id` (order_id); +ALTER TABLE `licenses` ADD INDEX `idx_licenses_status` (status); +ALTER TABLE `template_files` ADD INDEX `idx_template_files_account_id` (account_id); +ALTER TABLE `template_files` ADD INDEX `idx_template_files_file_name` (file_name(500)); + +-- +migrate Down +ALTER TABLE `accounts` DROP INDEX `idx_accounts_tier`; +ALTER TABLE `accounts` DROP INDEX `idx_accounts_parent_account_id`; +ALTER TABLE `users` DROP INDEX `idx_users_external_id`; +ALTER TABLE `users` DROP INDEX `idx_users_email_verified`; +ALTER TABLE `licenses` DROP INDEX `idx_licenses_order_id`; +ALTER TABLE `licenses` DROP INDEX `idx_licenses_status`; +ALTER TABLE `template_files` DROP INDEX `idx_template_files_account_id`; +ALTER TABLE `template_files` DROP INDEX `idx_template_files_file_name`; \ No newline at end of file diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index 511e4b8..6657210 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -219,9 +219,9 @@ export const PNS = { }; /** - * ユーザーのライセンス状態 + * ユーザーのライセンスの有効期限の状態 */ -export const USER_LICENSE_STATUS = { +export const USER_LICENSE_EXPIRY_STATUS = { NORMAL: 'Normal', NO_LICENSE: 'NoLicense', ALERT: 'Alert', @@ -311,3 +311,13 @@ export const USER_AUDIO_FORMAT = 'DS2(QP)'; * @const {string[]} */ export const NODE_ENV_TEST = 'test'; + +/** + * ユーザに対するライセンスの状態 + * @const {string[]} + */ +export const USER_LICENSE_STATUS = { + UNALLOCATED: 'unallocated', + ALLOCATED: 'allocated', + EXPIRED: 'expired', +} as const; diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index e0202f7..2648785 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -198,7 +198,7 @@ export class CancelIssueRequest { export class CreateWorktypesRequest { @ApiProperty({ minLength: 1, maxLength: 255, description: 'WorktypeID' }) @MinLength(1) - @MaxLength(255) + @MaxLength(16) @IsRecorderAllowed() worktypeId: string; @ApiProperty({ description: 'Worktypeの説明', required: false }) @@ -210,7 +210,7 @@ export class CreateWorktypesRequest { export class UpdateWorktypesRequest { @ApiProperty({ minLength: 1, description: 'WorktypeID' }) @MinLength(1) - @MaxLength(255) + @MaxLength(16) @IsRecorderAllowed() worktypeId: string; @ApiProperty({ description: 'Worktypeの説明', required: false }) diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index 17d1253..deff777 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -235,67 +235,6 @@ describe('publishUploadSas', () => { new HttpException(makeErrorResponse('E010812'), HttpStatus.BAD_REQUEST), ); }); - it('アップロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { - external_id: externalId, - id: userId, - author_id: authorId, - } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); - // 昨日の日付を作成 - let yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - yesterday = new DateWithZeroTime(yesterday); - // 期限切れのライセンスを作成して紐づける - await createLicense( - source, - 1, - yesterday, - tier5Accounts.account.id, - LICENSE_TYPE.NORMAL, - LICENSE_ALLOCATED_STATUS.ALLOCATED, - userId, - null, - null, - null, - ); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishUploadSas = `${url}?sas-token`; - blobParam.fileExists = false; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - await expect( - service.publishUploadSas( - makeContext('trackingId', 'requestId'), - externalId, - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST), - ); - }); }); describe('タスク作成から自動ルーティング(DB使用)', () => { @@ -1097,76 +1036,6 @@ describe('音声ファイルダウンロードURL取得', () => { ), ).toEqual(`${url}?sas-token`); }); - it('ダウンロードSASトークンが乗っているURLを取得できる(第五階層の場合ライセンスのチェックを行う)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { - external_id: externalId, - id: userId, - author_id: authorId, - } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); - // 本日の日付を作成 - let today = new Date(); - today.setDate(today.getDate()); - today = new DateWithZeroTime(today); - // 有効期限内のライセンスを作成して紐づける - await createLicense( - source, - 1, - today, - tier5Accounts.account.id, - LICENSE_TYPE.NORMAL, - LICENSE_ALLOCATED_STATUS.ALLOCATED, - userId, - null, - null, - null, - ); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - - const { audioFileId } = await createTask( - source, - tier5Accounts.account.id, - url, - 'test.zip', - 'InProgress', - undefined, - authorId ?? '', - ); - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishDownloadSas = `${url}?sas-token`; - blobParam.fileExists = true; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - expect( - await service.publishAudioFileDownloadSas( - makeContext('trackingId', 'requestId'), - externalId, - audioFileId, - ), - ).toEqual(`${url}?sas-token`); - }); it('Typistの場合、タスクのステータスが[Inprogress,Pending]以外でエラー', async () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); @@ -1396,133 +1265,6 @@ describe('音声ファイルダウンロードURL取得', () => { new HttpException(makeErrorResponse('E010701'), HttpStatus.BAD_REQUEST), ); }); - it('ダウンロード時にユーザーにライセンスが未割当の場合エラーとなる(第五階層限定)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する(ライセンスは作成しない) - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { - external_id: externalId, - id: userId, - author_id: authorId, - } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - - const { audioFileId } = await createTask( - source, - tier5Accounts.account.id, - url, - 'test.zip', - 'InProgress', - undefined, - authorId ?? '', - ); - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishDownloadSas = `${url}?sas-token`; - blobParam.fileExists = false; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - await expect( - service.publishAudioFileDownloadSas( - makeContext('trackingId', 'requestId'), - externalId, - audioFileId, - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010812'), HttpStatus.BAD_REQUEST), - ); - }); - it('ダウンロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { - external_id: externalId, - id: userId, - author_id: authorId, - } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); - // 昨日の日付を作成 - let yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - yesterday = new DateWithZeroTime(yesterday); - // 期限切れのライセンスを作成して紐づける - await createLicense( - source, - 1, - yesterday, - tier5Accounts.account.id, - LICENSE_TYPE.NORMAL, - LICENSE_ALLOCATED_STATUS.ALLOCATED, - userId, - null, - null, - null, - ); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - - const { audioFileId } = await createTask( - source, - tier5Accounts.account.id, - url, - 'test.zip', - 'InProgress', - undefined, - authorId ?? '', - ); - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishDownloadSas = `${url}?sas-token`; - blobParam.fileExists = false; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - await expect( - service.publishAudioFileDownloadSas( - makeContext('trackingId', 'requestId'), - externalId, - audioFileId, - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST), - ); - }); }); describe('テンプレートファイルダウンロードURL取得', () => { @@ -1596,70 +1338,7 @@ describe('テンプレートファイルダウンロードURL取得', () => { ); expect(resultUrl).toBe(`${url}?sas-token`); }); - it('ダウンロードSASトークンが乗っているURLを取得できる(第五階層の場合ライセンスのチェックを行う)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { external_id: externalId, id: userId } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'typist-user-external-id', - role: USER_ROLES.TYPIST, - }); - // 本日の日付を作成 - let yesterday = new Date(); - yesterday.setDate(yesterday.getDate()); - yesterday = new DateWithZeroTime(yesterday); - // 有効期限内のライセンスを作成して紐づける - await createLicense( - source, - 1, - yesterday, - tier5Accounts.account.id, - LICENSE_TYPE.NORMAL, - LICENSE_ALLOCATED_STATUS.ALLOCATED, - userId, - null, - null, - null, - ); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - const { audioFileId } = await createTask( - source, - tier5Accounts.account.id, - url, - 'test.zip', - TASK_STATUS.IN_PROGRESS, - userId, - 'AUTHOR_ID', - ); - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishDownloadSas = `${url}?sas-token`; - blobParam.fileExists = true; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - const resultUrl = await service.publishTemplateFileDownloadSas( - makeContext('trackingId', 'requestId'), - externalId, - audioFileId, - ); - expect(resultUrl).toBe(`${url}?sas-token`); - }); it('タスクのステータスが[Inprogress,Pending]以外でエラー', async () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); @@ -1849,135 +1528,6 @@ describe('テンプレートファイルダウンロードURL取得', () => { } } }); - it('ダウンロード時にユーザーにライセンスが未割当の場合エラーとなる(第五階層限定)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する(ライセンスは作成しない) - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { external_id: externalId, id: userId } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'typist-user-external-id', - role: USER_ROLES.TYPIST, - }); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - - const { audioFileId } = await createTask( - source, - tier5Accounts.account.id, - url, - 'test.zip', - TASK_STATUS.IN_PROGRESS, - undefined, - 'AUTHOR_ID', - ); - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishDownloadSas = `${url}?sas-token`; - blobParam.fileExists = false; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - try { - await service.publishTemplateFileDownloadSas( - makeContext('trackingId', 'requestId'), - externalId, - audioFileId, - ); - fail(); - } catch (e) { - if (e instanceof HttpException) { - expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); - expect(e.getResponse()).toEqual(makeErrorResponse('E010812')); - } else { - fail(); - } - } - }); - it('ダウンロード時にユーザーに割り当てられたライセンスが有効期限切れの場合エラー(第五階層限定)', async () => { - if (!source) fail(); - // 第五階層のアカウントまで作成し、そのアカウントに紐づくユーザーを作成する - const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( - source, - ); - const tier5Accounts = await makeTestAccount(source, { - parent_account_id: tier4Accounts[0].account.id, - tier: 5, - }); - const { external_id: externalId, id: userId } = await makeTestUser(source, { - account_id: tier5Accounts.account.id, - external_id: 'typist-user-external-id', - role: USER_ROLES.TYPIST, - }); - // 昨日の日付を作成 - let yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - yesterday = new DateWithZeroTime(yesterday); - // 期限切れのライセンスを作成して紐づける - await createLicense( - source, - 1, - yesterday, - tier5Accounts.account.id, - LICENSE_TYPE.NORMAL, - LICENSE_ALLOCATED_STATUS.ALLOCATED, - userId, - null, - null, - null, - ); - const url = `https://saodmsusdev.blob.core.windows.net/account-${tier5Accounts.account.id}/${userId}`; - - const { audioFileId } = await createTask( - source, - tier5Accounts.account.id, - url, - 'test.zip', - TASK_STATUS.IN_PROGRESS, - undefined, - 'AUTHOR_ID', - ); - - const blobParam = makeBlobstorageServiceMockValue(); - blobParam.publishDownloadSas = `${url}?sas-token`; - blobParam.fileExists = false; - - const notificationParam = makeDefaultNotificationhubServiceMockValue(); - const module = await makeTestingModuleWithBlobAndNotification( - source, - blobParam, - notificationParam, - ); - if (!module) fail(); - const service = module.get(FilesService); - - try { - await service.publishTemplateFileDownloadSas( - makeContext('trackingId', 'requestId'), - externalId, - audioFileId, - ), - fail(); - } catch (e) { - if (e instanceof HttpException) { - expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); - expect(e.getResponse()).toEqual(makeErrorResponse('E010805')); - } else { - fail(); - } - } - }); }); describe('publishTemplateFileUploadSas', () => { diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts index 6c1b558..1f9c042 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -8,6 +8,7 @@ import { OPTION_ITEM_NUM, TASK_STATUS, TIERS, + USER_LICENSE_STATUS, USER_ROLES, } from '../../constants/index'; import { User } from '../../repositories/users/entity/user.entity'; @@ -308,10 +309,10 @@ export class FilesService { context, user.id, ); - if (state === 'expired') { + if (state === USER_LICENSE_STATUS.EXPIRED) { throw new LicenseExpiredError('license is expired.'); } - if (state === 'inallocated') { + if (state === USER_LICENSE_STATUS.UNALLOCATED) { throw new LicenseNotAllocatedError('license is not allocated.'); } } @@ -392,20 +393,6 @@ export class FilesService { if (!user.account) { throw new AccountNotFoundError('account not found.'); } - // 第五階層のみチェック - if (user.account.tier === TIERS.TIER5) { - // ライセンスが有効でない場合、エラー - const { state } = await this.licensesRepository.getLicenseState( - context, - user.id, - ); - if (state === 'expired') { - throw new LicenseExpiredError('license is expired.'); - } - if (state === 'inallocated') { - throw new LicenseNotAllocatedError('license is not allocated.'); - } - } accountId = user.account.id; userId = user.id; country = user.account.country; @@ -422,16 +409,6 @@ export class FilesService { }`, ); switch (e.constructor) { - case LicenseExpiredError: - throw new HttpException( - makeErrorResponse('E010805'), - HttpStatus.BAD_REQUEST, - ); - case LicenseNotAllocatedError: - throw new HttpException( - makeErrorResponse('E010812'), - HttpStatus.BAD_REQUEST, - ); default: throw new HttpException( makeErrorResponse('E009999'), @@ -571,20 +548,6 @@ export class FilesService { if (!user.account) { throw new AccountNotFoundError('account not found.'); } - // 第五階層のみチェック - if (user.account.tier === TIERS.TIER5) { - // ライセンスが有効でない場合、エラー - const { state } = await this.licensesRepository.getLicenseState( - context, - user.id, - ); - if (state === 'expired') { - throw new LicenseExpiredError('license is expired.'); - } - if (state === 'inallocated') { - throw new LicenseNotAllocatedError('license is not allocated.'); - } - } accountId = user.account_id; userId = user.id; country = user.account.country; @@ -596,16 +559,6 @@ export class FilesService { }`, ); switch (e.constructor) { - case LicenseExpiredError: - throw new HttpException( - makeErrorResponse('E010805'), - HttpStatus.BAD_REQUEST, - ); - case LicenseNotAllocatedError: - throw new HttpException( - makeErrorResponse('E010812'), - HttpStatus.BAD_REQUEST, - ); default: throw new HttpException( makeErrorResponse('E009999'), diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts index c3473ab..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(); } diff --git a/dictation_server/src/features/licenses/licenses.service.ts b/dictation_server/src/features/licenses/licenses.service.ts index 5b5ef19..3f64729 100644 --- a/dictation_server/src/features/licenses/licenses.service.ts +++ b/dictation_server/src/features/licenses/licenses.service.ts @@ -80,7 +80,9 @@ export class LicensesService { .parent_account_id ?? undefined; // 親アカウントIDが取得できない場合はエラー if (parentAccountId === undefined) { - throw new Error('parent account id is undefined'); + throw new AccountNotFoundError( + `parent account id is not found. myAccountId: ${myAccountId}`, + ); } } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); @@ -147,6 +149,7 @@ export class LicensesService { ); } } + async issueCardLicenseKeys( context: Context, externalId: string, diff --git a/dictation_server/src/features/tasks/tasks.module.ts b/dictation_server/src/features/tasks/tasks.module.ts index 5a44eb9..67583a2 100644 --- a/dictation_server/src/features/tasks/tasks.module.ts +++ b/dictation_server/src/features/tasks/tasks.module.ts @@ -9,6 +9,7 @@ import { NotificationhubModule } from '../../gateways/notificationhub/notificati import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module'; import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module'; +import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module NotificationhubModule, SendGridModule, BlobstorageModule, + 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 f0de160..e417991 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -27,7 +27,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'; @@ -42,6 +48,9 @@ import { TasksRepositoryService } from '../../repositories/tasks/tasks.repositor import { overrideBlobstorageService } from '../../common/test/overrides'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.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 () => { @@ -52,12 +61,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'; @@ -126,6 +138,8 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); usersRepositoryMockValue.findUserByExternalId = new Error('DB failed'); const service = await makeTasksServiceMock( tasksRepositoryMockValue, @@ -133,6 +147,7 @@ describe('TasksService', () => { userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -168,6 +183,8 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); tasksRepositoryMockValue.getTasksFromAccountId = new Error('DB failed'); const service = await makeTasksServiceMock( tasksRepositoryMockValue, @@ -175,6 +192,7 @@ describe('TasksService', () => { userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -256,12 +274,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; @@ -296,6 +317,8 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); if (usersRepositoryMockValue.findUserByExternalId instanceof Error) { return; } @@ -306,6 +329,7 @@ describe('TasksService', () => { userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -380,6 +404,8 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); tasksRepositoryMockValue.getTasksFromAuthorIdAndAccountId = new Error( 'DB failed', ); @@ -389,6 +415,7 @@ describe('TasksService', () => { userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -424,6 +451,8 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); if (usersRepositoryMockValue.findUserByExternalId instanceof Error) { return; } @@ -435,6 +464,7 @@ describe('TasksService', () => { userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -512,12 +542,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'; @@ -553,6 +586,8 @@ describe('TasksService', () => { const adb2cServiceMockValue = makeDefaultAdb2cServiceMockValue(); const notificationhubServiceMockValue = makeDefaultNotificationhubServiceMockValue(); + const licensesRepositoryMockValue = + makeDefaultLicensesRepositoryMockValue(); adb2cServiceMockValue.getUsers = new Adb2cTooManyRequestsError(); const service = await makeTasksServiceMock( tasksRepositoryMockValue, @@ -560,6 +595,7 @@ describe('TasksService', () => { userGroupsRepositoryMockValue, adb2cServiceMockValue, notificationhubServiceMockValue, + licensesRepositoryMockValue, ); const userId = 'userId'; @@ -1636,7 +1672,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); @@ -1682,7 +1786,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 4b1b5d5..48dd0bb 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -13,6 +13,8 @@ import { ADMIN_ROLES, MANUAL_RECOVERY_REQUIRED, TASK_STATUS, + TIERS, + USER_LICENSE_STATUS, USER_ROLES, } from '../../constants'; import { @@ -42,6 +44,12 @@ import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.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 { @@ -55,6 +63,7 @@ export class TasksService { private readonly sendgridService: SendGridService, private readonly notificationhubService: NotificationhubService, private readonly blobStorageService: BlobstorageService, + private readonly licensesRepository: LicensesRepositoryService, ) {} async getTasks( @@ -283,9 +292,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) { @@ -315,6 +341,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 282691b..1a0f4ab 100644 --- a/dictation_server/src/features/tasks/test/tasks.service.mock.ts +++ b/dictation_server/src/features/tasks/test/tasks.service.mock.ts @@ -18,6 +18,11 @@ import { UserGroupsRepositoryService } from '../../../repositories/user_groups/u import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service'; import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service'; import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service'; +import { + LicensesRepositoryMockValue, + makeLicensesRepositoryMock, +} from '../../accounts/test/accounts.service.mock'; +import { LicensesRepositoryService } from '../../../repositories/licenses/licenses.repository.service'; export type TasksRepositoryMockValue = { getTasksFromAccountId: @@ -66,6 +71,7 @@ export const makeTasksServiceMock = async ( userGroupsRepositoryMockValue: UserGroupsRepositoryMockValue, adB2CServiceMockValue: AdB2CServiceMockValue, notificationhubServiceMockValue: NotificationhubServiceMockValue, + licensesRepositoryMockValue: LicensesRepositoryMockValue, ): Promise<{ tasksService: TasksService; taskRepoService: TasksRepositoryService; @@ -95,6 +101,8 @@ export const makeTasksServiceMock = async ( return {}; case BlobstorageService: 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 2e957a6..195e9c7 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 6f0f44d..8ab9061 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/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index 6d7c758..2832672 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -821,6 +821,7 @@ export class AccountsRepositoryService { status: Not(LICENSE_ALLOCATED_STATUS.UNALLOCATED), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // 存在した場合エラー @@ -1023,6 +1024,7 @@ export class AccountsRepositoryService { email_verified: true, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!primaryAdminUser) { throw new AdminUserNotFoundError( @@ -1040,6 +1042,7 @@ export class AccountsRepositoryService { email_verified: true, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!secondryAdminUser) { throw new AdminUserNotFoundError( diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 9f9cb85..a5e03a3 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -12,9 +12,9 @@ import { LICENSE_ALLOCATED_STATUS, LICENSE_ISSUE_STATUS, LICENSE_TYPE, - NODE_ENV_TEST, SWITCH_FROM_TYPE, TIERS, + USER_LICENSE_STATUS, } from '../../constants'; import { PoNumberAlreadyExistError, @@ -422,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) { // 注文が存在しない場合、エラー @@ -569,6 +566,7 @@ export class LicensesRepositoryService { id: newLicenseId, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // ライセンスが存在しない場合はエラー @@ -806,12 +804,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 +827,7 @@ export class LicensesRepositoryService { // ライセンスが割り当てられていない場合は未割当状態 if (allocatedLicense == null) { - return { state: 'inallocated' }; + return { state: USER_LICENSE_STATUS.UNALLOCATED }; } // ライセンスの有効期限が過ぎている場合は期限切れ状態 @@ -833,9 +836,9 @@ export class LicensesRepositoryService { allocatedLicense.expiry_date && allocatedLicense.expiry_date < currentDate ) { - return { state: 'expired' }; + return { state: USER_LICENSE_STATUS.EXPIRED }; } - return { state: 'allocated' }; + return { state: USER_LICENSE_STATUS.ALLOCATED }; } } diff --git a/dictation_server/src/repositories/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' }, }); // 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理