From 356f5fe3467ba5b0704a44606a526100c4f8a3cb Mon Sep 17 00:00:00 2001 From: "oura.a" Date: Thu, 10 Aug 2023 08:26:44 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20320:=20API=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=EF=BC=88=E3=83=A9=E3=82=A4=E3=82=BB=E3=83=B3=E3=82=B9=E5=89=B2?= =?UTF-8?q?=E3=82=8A=E5=BD=93=E3=81=A6API=EF=BC=89=5F=E5=B1=A5=E6=AD=B4?= =?UTF-8?q?=E4=BB=A5=E5=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2362: API実装(ライセンス割り当てAPI)_履歴以外](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2362) ライセンス割り当てのAPIを作成しました。 ※ライセンス割り当て履歴テーブルが絡む処理は別タスクでの対応となるので、ここでは未実装です。 ## レビューポイント なし ## UIの変更 なし ## 動作確認状況 ローカルでUT、動作確認実施済み ## 補足 なし --- dictation_server/src/common/error/code.ts | 2 + dictation_server/src/common/error/message.ts | 2 + dictation_server/src/constants/index.ts | 6 + .../accounts/accounts.service.spec.ts | 1 - .../src/features/accounts/test/utility.ts | 2 +- .../src/features/accounts/types/types.ts | 6 +- .../features/licenses/licenses.controller.ts | 1 - .../licenses/licenses.service.spec.ts | 142 +++++++++++++++++- .../src/features/licenses/test/utility.ts | 9 +- .../src/features/licenses/types/types.ts | 21 ++- .../features/users/test/users.service.mock.ts | 14 ++ .../src/features/users/types/types.ts | 2 +- .../src/features/users/users.controller.ts | 10 +- .../src/features/users/users.module.ts | 2 + .../src/features/users/users.service.spec.ts | 46 ++++++ .../src/features/users/users.service.ts | 53 +++++++ .../accounts/accounts.repository.service.ts | 20 +-- .../src/repositories/licenses/errors/types.ts | 5 + .../licenses/licenses.repository.service.ts | 63 ++++++++ 19 files changed, 383 insertions(+), 24 deletions(-) diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index bf06b11..43aec9f 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -44,4 +44,6 @@ export const ErrorCodes = [ 'E010802', // ライセンス取り込み済みエラー 'E010803', // ライセンス発行済みエラー 'E010804', // ライセンス不足エラー + 'E010805', // ライセンス有効期限切れエラー + 'E010806', // ライセンス割り当て不可エラー ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 3339120..e41ba72 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -33,4 +33,6 @@ export const errors: Errors = { E010802: 'License already activated Error', E010803: 'License already issued Error', E010804: 'License shortage Error', + E010805: 'License is expired Error', + E010806: 'License is unavailable Error', }; diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index 1a0852e..a4bd53b 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -126,6 +126,12 @@ export const LICENSE_ALLOCATED_STATUS = { */ export const LICENSE_EXPIRATION_THRESHOLD_DAYS = 14; +/** + * ライセンスの有効期間 + * @const {number} + */ +export const LICENSE_EXPIRATION_DAYS = 365; + /** * カードライセンスの桁数 * @const {number} diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 3c55958..4c523e8 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -580,7 +580,6 @@ describe('getPartnerAccount', () => { }); }); - describe('getOrderHistories', () => { let source: DataSource = null; beforeEach(async () => { diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index b9f3d80..31f4237 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -126,4 +126,4 @@ export const createUser = async ( }); const user = identifiers.pop() as User; return { userId: user.id, externalId: external_id, authorId: author_id }; -}; \ No newline at end of file +}; diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index f62c9a0..32ee1da 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -191,15 +191,15 @@ export class GetPartnerLicensesResponse { // 第五階層のshortage算出用 export class PartnerLicenseInfoForShortage { - expiringSoonLicense?:number; - allocatableLicenseWithMargin?:number; + expiringSoonLicense?: number; + allocatableLicenseWithMargin?: number; } // RepositoryからPartnerLicenseInfoに関する情報を取得する際の型 export type PartnerLicenseInfoForRepository = Omit< PartnerLicenseInfo & PartnerLicenseInfoForShortage, 'shortage' - >; +>; export class GetOrderHistoriesRequest { @ApiProperty({ description: '取得件数' }) diff --git a/dictation_server/src/features/licenses/licenses.controller.ts b/dictation_server/src/features/licenses/licenses.controller.ts index c119052..b5dea4a 100644 --- a/dictation_server/src/features/licenses/licenses.controller.ts +++ b/dictation_server/src/features/licenses/licenses.controller.ts @@ -202,7 +202,6 @@ export class LicensesController { @Req() req: Request, @Body() body: GetAllocatableLicensesRequest, ): Promise { - // TODO 仮の戻り値 return { allocatableLicenses: [ diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts index 553f2cb..644857c 100644 --- a/dictation_server/src/features/licenses/licenses.service.spec.ts +++ b/dictation_server/src/features/licenses/licenses.service.spec.ts @@ -4,6 +4,7 @@ import { IssueCardLicensesRequest, IssueCardLicensesResponse, ActivateCardLicensesRequest, + NewAllocatedLicenseExpirationDate, } from './types/types'; import { makeDefaultAccountsRepositoryMockValue, @@ -31,6 +32,8 @@ import { selectCardLicense, selectLicense, } from './test/utility'; +import { UsersService } from '../users/users.service'; +import { makeContext } from '../../common/log'; describe('LicensesService', () => { it('ライセンス注文が完了する', async () => { @@ -327,7 +330,14 @@ describe('DBテスト', () => { const licenseId = 50; const issueId = 100; - await createLicense(source, licenseId, defaultAccountId); + await createLicense( + source, + licenseId, + null, + defaultAccountId, + 'Unallocated', + null, + ); await createCardLicense(source, licenseId, issueId, cardLicenseKey); await createCardLicenseIssue(source, issueId); @@ -345,3 +355,133 @@ describe('DBテスト', () => { expect(dbSelectResultFromLicense.license.account_id).toEqual(accountId); }); }); + +describe('ライセンス割り当て', () => { + let source: DataSource = null; + beforeEach(async () => { + source = new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: false, + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + await source.destroy(); + source = null; + }); + + it('未割当のライセンスに対して、ライセンス割り当てが完了する', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { userId } = await createUser(source, accountId, 'userId', 'admin'); + await createLicense(source, 1, null, accountId, 'Unallocated', null); + + const service = module.get(UsersService); + + const expiry_date = new NewAllocatedLicenseExpirationDate(); + + await service.allocateLicense(makeContext('trackingId'), userId, 1); + const result = await selectLicense(source, 1); + expect(result.license.allocated_user_id).toBe(userId); + expect(result.license.status).toBe('Allocated'); + expect(result.license.expiry_date.setMilliseconds(0)).toEqual( + expiry_date.setMilliseconds(0), + ); + }); + + it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { userId } = await createUser(source, accountId, 'userId', 'admin'); + const date = new Date(); + date.setDate(date.getDate() + 30); + await createLicense(source, 1, date, accountId, 'Reusable', null); + + const service = module.get(UsersService); + + await service.allocateLicense(makeContext('trackingId'), userId, 1); + const result = await selectLicense(source, 1); + expect(result.license.allocated_user_id).toBe(userId); + expect(result.license.status).toBe('Allocated'); + expect(result.license.expiry_date).toEqual(date); + }); + + it('未割当のライセンスに対して、別のライセンスが割り当てられているユーザーの割り当てが完了する', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { userId } = await createUser(source, accountId, 'userId', 'admin'); + const date = new Date(); + date.setDate(date.getDate() + 30); + await createLicense(source, 1, date, accountId, 'Allocated', userId); + await createLicense(source, 2, null, accountId, 'Unallocated', null); + + const service = module.get(UsersService); + + const expiry_date = new NewAllocatedLicenseExpirationDate(); + + await service.allocateLicense(makeContext('trackingId'), userId, 2); + + // もともと割り当てられていたライセンスの状態確認 + const result1 = await selectLicense(source, 1); + expect(result1.license.allocated_user_id).toBe(null); + expect(result1.license.status).toBe('Reusable'); + expect(result1.license.expiry_date).toEqual(date); + + // 新たに割り当てたライセンスの状態確認 + const result2 = await selectLicense(source, 2); + expect(result2.license.allocated_user_id).toBe(userId); + expect(result2.license.status).toBe('Allocated'); + expect(result2.license.expiry_date.setMilliseconds(0)).toEqual( + expiry_date.setMilliseconds(0), + ); + }); + + it('有効期限が切れているライセンスを割り当てようとした場合、エラーになる', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { userId } = await createUser(source, accountId, 'userId', 'admin'); + const date = new Date(); + date.setDate(date.getDate() - 30); + await createLicense(source, 1, date, accountId, 'Reusable', null); + + const service = module.get(UsersService); + + await expect( + service.allocateLicense(makeContext('trackingId'), userId, 1), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010805'), HttpStatus.BAD_REQUEST), + ); + }); + + it('割り当て不可なライセンスを割り当てようとした場合、エラーになる', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { userId } = await createUser(source, accountId, 'userId', 'admin'); + const date = new Date(); + date.setDate(date.getDate() + 30); + await createLicense(source, 1, null, accountId, 'Allocated', null); + await createLicense(source, 2, null, accountId, 'Deleted', null); + + const service = module.get(UsersService); + + await expect( + service.allocateLicense(makeContext('trackingId'), userId, 1), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010806'), HttpStatus.BAD_REQUEST), + ); + await expect( + service.allocateLicense(makeContext('trackingId'), userId, 2), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010806'), HttpStatus.BAD_REQUEST), + ); + }); +}); diff --git a/dictation_server/src/features/licenses/test/utility.ts b/dictation_server/src/features/licenses/test/utility.ts index 7ff55f0..c7bfb80 100644 --- a/dictation_server/src/features/licenses/test/utility.ts +++ b/dictation_server/src/features/licenses/test/utility.ts @@ -58,15 +58,18 @@ export const createUser = async ( export const createLicense = async ( datasource: DataSource, licenseId: number, + expiry_date: Date, accountId: number, + status: string, + allocated_user_id: number, ): Promise => { const { identifiers } = await datasource.getRepository(License).insert({ id: licenseId, - expiry_date: null, + expiry_date: expiry_date, account_id: accountId, type: 'card', - status: 'Unallocated', - allocated_user_id: null, + status: status, + allocated_user_id: allocated_user_id, order_id: null, deleted_at: null, delete_order_id: null, diff --git a/dictation_server/src/features/licenses/types/types.ts b/dictation_server/src/features/licenses/types/types.ts index 71d8344..d5e2fce 100644 --- a/dictation_server/src/features/licenses/types/types.ts +++ b/dictation_server/src/features/licenses/types/types.ts @@ -1,6 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsInt, Matches, Max, Min, Length } from 'class-validator'; -import { LICENSE_EXPIRATION_THRESHOLD_DAYS } from '../../../constants'; +import { + LICENSE_EXPIRATION_DAYS, + LICENSE_EXPIRATION_THRESHOLD_DAYS, +} from '../../../constants'; export class CreateOrdersRequest { @ApiProperty() @@ -74,4 +77,18 @@ export class ExpirationThresholdDate extends Date { this.setDate(this.getDate() + LICENSE_EXPIRATION_THRESHOLD_DAYS); this.setHours(23, 59, 59, 999); // 時分秒を"23:59:59.999"に固定 } -} \ No newline at end of file +} + +// 新規ライセンス割り当て時の有効期限算出用に、365日後の日付を取得する +export class NewAllocatedLicenseExpirationDate extends Date { + constructor(...args: any[]) { + if (args.length === 0) { + super(); // 引数がない場合、現在の日付で初期化 + } else { + super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す + } + this.setDate(this.getDate() + LICENSE_EXPIRATION_DAYS); + this.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定 + this.setMilliseconds(0); + } +} diff --git a/dictation_server/src/features/users/test/users.service.mock.ts b/dictation_server/src/features/users/test/users.service.mock.ts index 8076a52..c3534c6 100644 --- a/dictation_server/src/features/users/test/users.service.mock.ts +++ b/dictation_server/src/features/users/test/users.service.mock.ts @@ -8,6 +8,7 @@ import { import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service'; import { User } from '../../../repositories/users/entity/user.entity'; import { UsersRepositoryService } from '../../../repositories/users/users.repository.service'; +import { LicensesRepositoryService } from '../../../repositories/licenses/licenses.repository.service'; import { UsersService } from '../users.service'; import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity'; import { SortCriteriaRepositoryService } from '../../../repositories/sort_criteria/sort_criteria.repository.service'; @@ -31,6 +32,10 @@ export type UsersRepositoryMockValue = { existsAuthorId: boolean | Error; }; +export type LicensesRepositoryMockValue = { + // empty +}; + export type AdB2cMockValue = { getMetaData: B2cMetadata | Error; getSignKeySets: JwkSignKey[] | Error; @@ -58,6 +63,7 @@ export type ConfigMockValue = { export const makeUsersServiceMock = async ( usersRepositoryMockValue: UsersRepositoryMockValue, + licensesRepositoryMockValue: LicensesRepositoryMockValue, adB2cMockValue: AdB2cMockValue, sendGridMockValue: SendGridMockValue, configMockValue: ConfigMockValue, @@ -75,6 +81,8 @@ export const makeUsersServiceMock = async ( switch (token) { case UsersRepositoryService: return makeUsersRepositoryMock(usersRepositoryMockValue); + case LicensesRepositoryService: + return makeLicensesRepositoryMock(); case AdB2cService: return makeAdB2cServiceMock(adB2cMockValue); case SendGridService: @@ -239,6 +247,12 @@ export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => { }; }; +export const makeLicensesRepositoryMock = (): LicensesRepositoryMockValue => { + return { + // empty + }; +}; + export const makeSendGridMock = (value: SendGridMockValue) => { const { sendMail, createMailContentFromEmailConfirmForNormalUser } = value; diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index 135d77b..993b38c 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -247,4 +247,4 @@ export class AllocateLicenseRequest { newLicenseId: number; } -export class AllocateLicenseResponse{} \ No newline at end of file +export class AllocateLicenseResponse {} diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index a4277c0..10a7b63 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -409,7 +409,15 @@ export class UsersController { @Body() body: AllocateLicenseRequest, @Req() req: Request, ): Promise { + const accessToken = retrieveAuthorizationToken(req); + const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken; + + const context = makeContext(userId); + await this.usersService.allocateLicense( + context, + body.userId, + body.newLicenseId, + ); return {}; } } - diff --git a/dictation_server/src/features/users/users.module.ts b/dictation_server/src/features/users/users.module.ts index febd59f..e4ae006 100644 --- a/dictation_server/src/features/users/users.module.ts +++ b/dictation_server/src/features/users/users.module.ts @@ -4,12 +4,14 @@ import { AdB2cModule } from '../../gateways/adb2c/adb2c.module'; import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; import { SortCriteriaRepositoryModule } from '../../repositories/sort_criteria/sort_criteria.repository.module'; import { UsersRepositoryModule } from '../../repositories/users/users.repository.module'; +import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; @Module({ imports: [ UsersRepositoryModule, + LicensesRepositoryModule, SortCriteriaRepositoryModule, AdB2cModule, SendGridModule, diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index 2565b0d..5ef0969 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -30,6 +30,7 @@ import { makeTestingModule } from '../../common/test/modules'; describe('UsersService.confirmUser', () => { it('ユーザの仮登録時に払い出されるトークンにより、未認証のユーザが認証済みになる', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -37,6 +38,7 @@ describe('UsersService.confirmUser', () => { makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendGridMockValue, configMockValue, @@ -66,6 +68,7 @@ describe('UsersService.confirmUser', () => { encryption: false, prompt: false, }; + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const configMockValue = makeDefaultConfigValue(); const sortCriteriaRepositoryMockValue = @@ -73,6 +76,7 @@ describe('UsersService.confirmUser', () => { const sendGridMockValue = makeDefaultSendGridlValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendGridMockValue, configMockValue, @@ -86,6 +90,7 @@ describe('UsersService.confirmUser', () => { it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -93,6 +98,7 @@ describe('UsersService.confirmUser', () => { makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -123,6 +129,7 @@ describe('UsersService.confirmUser', () => { encryption: false, prompt: false, }; + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -130,6 +137,7 @@ describe('UsersService.confirmUser', () => { makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendGridMockValue, configMockValue, @@ -142,6 +150,7 @@ describe('UsersService.confirmUser', () => { }); it('ユーザが既に認証済みだった場合、認証済みユーザエラーとなる。', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -153,6 +162,7 @@ describe('UsersService.confirmUser', () => { const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -183,6 +193,7 @@ describe('UsersService.confirmUser', () => { encryption: false, prompt: false, }; + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendGridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -193,6 +204,7 @@ describe('UsersService.confirmUser', () => { const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendGridMockValue, configMockValue, @@ -206,6 +218,7 @@ describe('UsersService.confirmUser', () => { }); it('DBネットワークエラーとなる場合、エラーとなる。', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -215,6 +228,7 @@ describe('UsersService.confirmUser', () => { const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -248,6 +262,7 @@ describe('UsersService.confirmUser', () => { encryption: false, prompt: false, }; + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendGridMockValue = makeDefaultSendGridlValue(); usersRepositoryMockValue.updateUserVerified = new Error('DB error'); @@ -256,6 +271,7 @@ describe('UsersService.confirmUser', () => { makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendGridMockValue, configMockValue, @@ -272,6 +288,7 @@ describe('UsersService.confirmUser', () => { }); it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:None)', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -279,6 +296,7 @@ describe('UsersService.confirmUser', () => { makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -308,6 +326,7 @@ describe('UsersService.confirmUser', () => { describe('UsersService.createUser', () => { it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化あり)', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -315,6 +334,7 @@ describe('UsersService.createUser', () => { makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -350,6 +370,7 @@ describe('UsersService.createUser', () => { it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Author; 暗号化無し)', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -357,6 +378,7 @@ describe('UsersService.createUser', () => { makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -392,6 +414,7 @@ describe('UsersService.createUser', () => { it('管理者権限のあるアクセストークンを使用して、新規ユーザが追加される(role:Transcriptioninst)', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -399,6 +422,7 @@ describe('UsersService.createUser', () => { makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -426,6 +450,7 @@ describe('UsersService.createUser', () => { }); it('DBネットワークエラーとなる場合、エラーとなる。', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -434,6 +459,7 @@ describe('UsersService.createUser', () => { usersRepositoryMockValue.createNormalUser = new Error('DB error'); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -465,6 +491,7 @@ describe('UsersService.createUser', () => { }); it('Azure ADB2Cでネットワークエラーとなる場合、エラーとなる。', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); adb2cParam.createUser = new Error(); const sendgridMockValue = makeDefaultSendGridlValue(); @@ -473,6 +500,7 @@ describe('UsersService.createUser', () => { makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -504,6 +532,7 @@ describe('UsersService.createUser', () => { }); it('メールアドレスが重複している場合、エラーとなる。', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); adb2cParam.createUser = { reason: 'email', message: 'ObjectConflict' }; const sendgridMockValue = makeDefaultSendGridlValue(); @@ -512,6 +541,7 @@ describe('UsersService.createUser', () => { makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -540,6 +570,7 @@ describe('UsersService.createUser', () => { }); it('AuthorIDが重複している場合、エラーとなる。(AuthorID重複チェックでエラー)', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -550,6 +581,7 @@ describe('UsersService.createUser', () => { const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -580,6 +612,7 @@ describe('UsersService.createUser', () => { }); it('AuthorIDが重複している場合、エラーとなる。(insert失敗)', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -590,6 +623,7 @@ describe('UsersService.createUser', () => { const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -882,6 +916,7 @@ describe('UsersService.getUsers', () => { describe('UsersService.updateSortCriteria', () => { it('ソート条件を変更できる', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -889,6 +924,7 @@ describe('UsersService.updateSortCriteria', () => { makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -906,6 +942,7 @@ describe('UsersService.updateSortCriteria', () => { it('ユーザー情報が存在せず、ソート条件を変更できない', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -916,6 +953,7 @@ describe('UsersService.updateSortCriteria', () => { const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -938,6 +976,7 @@ describe('UsersService.updateSortCriteria', () => { it('ソート条件が存在せず、ソート条件を変更できない', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -949,6 +988,7 @@ describe('UsersService.updateSortCriteria', () => { const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -973,6 +1013,7 @@ describe('UsersService.updateSortCriteria', () => { describe('UsersService.getSortCriteria', () => { it('ソート条件を取得できる', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -980,6 +1021,7 @@ describe('UsersService.getSortCriteria', () => { makeDefaultSortCriteriaRepositoryMockValue(); const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -997,6 +1039,7 @@ describe('UsersService.getSortCriteria', () => { it('ソート条件が存在せず、ソート条件を取得できない', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -1009,6 +1052,7 @@ describe('UsersService.getSortCriteria', () => { const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, @@ -1031,6 +1075,7 @@ describe('UsersService.getSortCriteria', () => { it('DBから取得した値が不正だった場合、エラーとなる', async () => { const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); + const licensesRepositoryMockValue = null; const adb2cParam = makeDefaultAdB2cMockValue(); const sendgridMockValue = makeDefaultSendGridlValue(); const configMockValue = makeDefaultConfigValue(); @@ -1045,6 +1090,7 @@ describe('UsersService.getSortCriteria', () => { const service = await makeUsersServiceMock( usersRepositoryMockValue, + licensesRepositoryMockValue, adb2cParam, sendgridMockValue, configMockValue, diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 146c416..3e3752a 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -23,6 +23,7 @@ import { newUser, } from '../../repositories/users/entity/user.entity'; import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; +import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; import { GetRelationsResponse, User } from './types/types'; import { AuthorIdAlreadyExistsError, @@ -39,11 +40,16 @@ import { import { DateWithZeroTime } from '../licenses/types/types'; import { Context } from '../../common/log'; import { UserRoles } from '../../common/types/role'; +import { + LicenseExpiredError, + LicenseUnavailableError, +} from '../../repositories/licenses/errors/types'; @Injectable() export class UsersService { constructor( private readonly usersRepository: UsersRepositoryService, + private readonly licensesRepository: LicensesRepositoryService, private readonly sortCriteriaRepository: SortCriteriaRepositoryService, private readonly adB2cService: AdB2cService, private readonly configService: ConfigService, @@ -805,4 +811,51 @@ export class UsersService { this.logger.log(`[OUT] [${context.trackingId}] ${this.updateUser.name}`); } } + + /** + * ライセンスをユーザーに割り当てます + * @param context + * @param userId + * @param newLicenseId + */ + async allocateLicense( + context: Context, + userId: number, + newLicenseId: number, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.allocateLicense.name} | params: { ` + + `userId: ${userId}, ` + + `newLicenseId: ${newLicenseId}, `, + ); + + try { + await this.licensesRepository.allocateLicense(userId, newLicenseId); + } catch (e) { + this.logger.error(`error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case LicenseExpiredError: + throw new HttpException( + makeErrorResponse('E010805'), + HttpStatus.BAD_REQUEST, + ); + case LicenseUnavailableError: + throw new HttpException( + makeErrorResponse('E010806'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.allocateLicense.name}`, + ); + } + } } diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index f7ecd96..631556e 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -507,16 +507,16 @@ export class AccountsRepositoryService { // 戻り値用の値を設定 const childPartnerLicenseFromRepository: PartnerLicenseInfoForRepository = - { - accountId: childAccount.id, - tier: childAccount.tier, - companyName: childAccount.company_name, - stockLicense: childLicenseOrderStatus.stockLicense, - issuedRequested: childLicenseOrderStatus.issuedRequested, - issueRequesting: childLicenseOrderStatus.issueRequesting, - expiringSoonLicense: expiringSoonLicense, - allocatableLicenseWithMargin: allocatableLicenseWithMargin, - }; + { + accountId: childAccount.id, + tier: childAccount.tier, + companyName: childAccount.company_name, + stockLicense: childLicenseOrderStatus.stockLicense, + issuedRequested: childLicenseOrderStatus.issuedRequested, + issueRequesting: childLicenseOrderStatus.issueRequesting, + expiringSoonLicense: expiringSoonLicense, + allocatableLicenseWithMargin: allocatableLicenseWithMargin, + }; childPartnerLicensesFromRepository.push( childPartnerLicenseFromRepository, diff --git a/dictation_server/src/repositories/licenses/errors/types.ts b/dictation_server/src/repositories/licenses/errors/types.ts index ae3ac02..e0f9fbe 100644 --- a/dictation_server/src/repositories/licenses/errors/types.ts +++ b/dictation_server/src/repositories/licenses/errors/types.ts @@ -13,3 +13,8 @@ export class OrderNotFoundError extends Error {} export class AlreadyIssuedError extends Error {} // ライセンス不足エラー export class LicensesShortageError extends Error {} + +// ライセンス有効期限切れエラー +export class LicenseExpiredError extends Error {} +// ライセンス割り当て不可エラー +export class LicenseUnavailableError extends Error {} diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 0c3c6ce..ab8bb6a 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -21,7 +21,10 @@ import { LicensesShortageError, AlreadyIssuedError, OrderNotFoundError, + LicenseExpiredError, + LicenseUnavailableError, } from './errors/types'; +import { NewAllocatedLicenseExpirationDate } from '../../features/licenses/types/types'; @Injectable() export class LicensesRepositoryService { @@ -387,4 +390,64 @@ export class LicensesRepositoryService { } }); } + + /** + * ライセンスをユーザーに割り当てる + * @param userId + * @param newLicenseId + */ + async allocateLicense(userId: number, newLicenseId: number): Promise { + await this.dataSource.transaction(async (entityManager) => { + // 割り当て対象のライセンス情報を取得 + const licenseRepo = entityManager.getRepository(License); + const targetLicense = await licenseRepo.findOne({ + where: { + id: newLicenseId, + }, + }); + + // 期限切れの場合はエラー + if (targetLicense.expiry_date) { + const currentDay = new Date(); + currentDay.setHours(23, 59, 59, 999); + if (targetLicense.expiry_date < currentDay) { + throw new LicenseExpiredError( + `License is expired. expiration date: ${targetLicense.expiry_date} current Date: ${currentDay}`, + ); + } + } + // ライセンス状態が「未割当」「再利用可能」以外の場合はエラー + if ( + targetLicense.status === LICENSE_ALLOCATED_STATUS.ALLOCATED || + targetLicense.status === LICENSE_ALLOCATED_STATUS.DELETED + ) { + throw new LicenseUnavailableError( + `License is unavailable. License status: ${targetLicense.status}`, + ); + } + + // 対象ユーザーのライセンス割り当て状態を取得 + const allocatedLicense = await licenseRepo.findOne({ + where: { + allocated_user_id: userId, + }, + }); + + // 既にライセンスが割り当てられているなら、割り当てを解除 + if (allocatedLicense) { + allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE; + allocatedLicense.allocated_user_id = null; + await licenseRepo.save(allocatedLicense); + } + + // ライセンス割り当てを実施 + targetLicense.status = LICENSE_ALLOCATED_STATUS.ALLOCATED; + targetLicense.allocated_user_id = userId; + // 有効期限が未設定なら365日後に設定 + if (!targetLicense.expiry_date) { + targetLicense.expiry_date = new NewAllocatedLicenseExpirationDate(); + } + await licenseRepo.save(targetLicense); + }); + } }