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 }; } }