From 1843844c48ec8f5706eee9eee5f1dd01622328ae Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Tue, 22 Aug 2023 08:55:56 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20321:=20API=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=EF=BC=88=E5=89=B2=E3=82=8A=E5=BD=93=E3=81=A6=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=82=BB=E3=83=B3=E3=82=B9=E5=8F=96=E5=BE=97?= =?UTF-8?q?API=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task2361: API実装(割り当て可能ライセンス取得API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2361) - 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず) - 何をどう変更したか、追加したライブラリなど - このPull Requestでの対象/対象外 - 影響範囲(他の機能にも影響があるか) メモリDB上にライセンスを作成するメソッドの有効期限を指定可能にしたため、 既存テストで特に指定していなかった箇所(デフォルトでnull)はnullを設定。 ## レビューポイント - 特にレビューしてほしい箇所 - 軽微なものや自明なものは記載不要 - 修正範囲が大きい場合などに記載 - 全体的にや仕様を満たしているか等は本当に必要な時のみ記載 取得レコードのソートの関係上、EntityManagerではなく、QueryBuilderでの実装としているが、問題ないか。 ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認 ・正常系 ①メモリDB上に5件の有効なライセンスを作成。無効なライセンスを4件作成。  ※内2件が有効期限null、2件が同一の有効期限、1件が最も遠い有効期限  有効なライセンスのみ取得できることを確認。  serviceの戻り値として、ソートされて取得できることを確認。  (nullが最優先、有効期限の降順、同一の有効期限のものはidで昇順) ②結果が0件(別のアカウントにはライセンスが存在する)の場合(POSTMAN)  正常終了し、空の配列を返却すること。  別のアカウントのライセンスを取得しないこと。 ・異常 ①tier5以外のアカウントで実行(POSTMAN)し、"Token authority failed Error."エラーになることを確認。 ②コンテナを停止して実行(POSTMAN)し、"Internal Server Error."エラーとなることを確認。 ・他 ①ログがポリシに従って出ていることの確認 ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/api/odms/openapi.json | 11 -- .../features/licenses/licenses.controller.ts | 24 ++-- .../licenses/licenses.service.spec.ts | 117 ++++++++++++++++++ .../src/features/licenses/licenses.service.ts | 46 ++++++- .../licenses/licenses.repository.service.ts | 53 +++++++- 5 files changed, 227 insertions(+), 24 deletions(-) diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index f156a85..2c10dd6 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -1934,16 +1934,6 @@ "summary": "", "description": "割り当て可能なライセンスを取得します", "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetAllocatableLicensesRequest" - } - } - } - }, "responses": { "200": { "description": "成功時のレスポンス", @@ -2850,7 +2840,6 @@ "required": ["cardLicenseKey"] }, "ActivateCardLicensesResponse": { "type": "object", "properties": {} }, - "GetAllocatableLicensesRequest": { "type": "object", "properties": {} }, "AllocatableLicenseInfo": { "type": "object", "properties": { diff --git a/dictation_server/src/features/licenses/licenses.controller.ts b/dictation_server/src/features/licenses/licenses.controller.ts index 1cf8e6d..f599211 100644 --- a/dictation_server/src/features/licenses/licenses.controller.ts +++ b/dictation_server/src/features/licenses/licenses.controller.ts @@ -32,6 +32,7 @@ import { AuthGuard } from '../../common/guards/auth/authguards'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { ADMIN_ROLES, TIERS } from '../../constants'; import jwt from 'jsonwebtoken'; +import { makeContext } from '../../common/log'; @ApiTags('licenses') @Controller('licenses') @@ -201,17 +202,18 @@ export class LicensesController { async getAllocatableLicenses( // eslint-disable-next-line @typescript-eslint/no-unused-vars @Req() req: Request, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @Body() body: GetAllocatableLicensesRequest, ): Promise { - // TODO 仮の戻り値 - return { - allocatableLicenses: [ - { licenseId: 1, expiryDate: null }, - { licenseId: 2, expiryDate: null }, - { licenseId: 3, expiryDate: new Date(2023, 12, 31, 23, 59, 59) }, - { licenseId: 4, expiryDate: new Date(2023, 10, 31, 23, 59, 59) }, - ], - }; + const token = retrieveAuthorizationToken(req); + const payload = jwt.decode(token, { json: true }) as AccessToken; + + const context = makeContext(payload.userId); + + const allocatableLicenses = + await this.licensesService.getAllocatableLicenses( + context, + payload.userId, + ); + + 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 2e3e46c..55e0e96 100644 --- a/dictation_server/src/features/licenses/licenses.service.spec.ts +++ b/dictation_server/src/features/licenses/licenses.service.spec.ts @@ -359,6 +359,123 @@ describe('DBテスト', () => { ).toBeDefined(); expect(dbSelectResultFromLicense.license.account_id).toEqual(accountId); }); + + it('取込可能なライセンスのみが取得できる', async () => { + const module = await makeTestingModule(source); + + const now = new Date(); + const { accountId } = await createAccount(source); + const { externalId } = await createUser( + source, + accountId, + 'userId', + 'admin', + ); + + // ライセンスを作成する + // 1件目 + await createLicense( + source, + 1, + new Date(now.getTime() + 60 * 60 * 1000), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + ); + // 2件目、expiry_dateがnull(OneYear) + await createLicense( + source, + 2, + null, + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + ); + // 3件目、1件目と同じ有効期限 + await createLicense( + source, + 3, + new Date(now.getTime() + 60 * 60 * 1000), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + ); + // 4件目、expiry_dateが一番遠いデータ + await createLicense( + source, + 4, + new Date(now.getTime() + 60 * 60 * 1000 * 2), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + ); + // 5件目、expiry_dateがnull(OneYear) + await createLicense( + source, + 5, + null, + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + ); + // 6件目、ライセンス状態が割当済 + await createLicense( + source, + 6, + new Date(now.getTime() + 60 * 60 * 1000 * 2), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + ); + // 7件目、ライセンス状態が削除済 + await createLicense( + source, + 7, + new Date(now.getTime() + 60 * 60 * 1000 * 2), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.DELETED, + null, + ); + // 8件目、別アカウントの未割当のライセンス + await createLicense( + source, + 8, + new Date(now.getTime() + 60 * 60 * 1000), + accountId + 1, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + ); + // 9件目、有効期限切れのライセンス + await createLicense( + source, + 9, + new Date(now.getTime() - 60 * 60 * 1000 * 24), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + ); + const service = module.get(LicensesService); + const context = makeContext('userId'); + const response = await service.getAllocatableLicenses(context, externalId); + // 対象外のデータは取得していないことを確認する + expect(response.allocatableLicenses.length).toBe(5); + // ソートして取得されていることを確認する + // (expiry_dateがnullを最優先、次に有効期限が遠い順(同じ有効期限の場合はID昇順) + expect(response.allocatableLicenses[0].licenseId).toBe(2); + expect(response.allocatableLicenses[1].licenseId).toBe(5); + expect(response.allocatableLicenses[2].licenseId).toBe(4); + expect(response.allocatableLicenses[3].licenseId).toBe(1); + expect(response.allocatableLicenses[4].licenseId).toBe(3); + }); }); describe('ライセンス割り当て', () => { diff --git a/dictation_server/src/features/licenses/licenses.service.ts b/dictation_server/src/features/licenses/licenses.service.ts index 2c13742..a60e4fd 100644 --- a/dictation_server/src/features/licenses/licenses.service.ts +++ b/dictation_server/src/features/licenses/licenses.service.ts @@ -11,7 +11,11 @@ import { } from '../../repositories/licenses/errors/types'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; import { UserNotFoundError } from '../../repositories/users/errors/types'; -import { IssueCardLicensesResponse } from './types/types'; +import { + GetAllocatableLicensesResponse, + IssueCardLicensesResponse, +} from './types/types'; +import { Context } from '../../common/log'; @Injectable() export class LicensesService { @@ -211,4 +215,44 @@ export class LicensesService { this.logger.log(`[OUT] ${this.activateCardLicenseKey.name}`); return; } + + /** + * get allocatable lisences + * @param context + * @param userId + * @@returns AllocatableLicenseInfo[] + */ + async getAllocatableLicenses( + context: Context, + userId: string, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.getAllocatableLicenses.name} | params: { ` + + `userId: ${userId}, `, + ); + // ユーザIDからアカウントIDを取得する + try { + const myAccountId = ( + await this.usersRepository.findUserByExternalId(userId) + ).account_id; + // 割り当て可能なライセンスを取得する + const allocatableLicenses = + await this.licensesRepository.getAllocatableLicenses(myAccountId); + + return { + allocatableLicenses, + }; + } catch (e) { + this.logger.error(`error=${e}`); + this.logger.error('get allocatable lisences failed'); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.getAllocatableLicenses.name}`, + ); + } + } } diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 694d48a..258da24 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -import { DataSource, In } from 'typeorm'; +import { DataSource, In, IsNull, MoreThanOrEqual } from 'typeorm'; import { LicenseOrder, License, @@ -26,6 +26,10 @@ import { LicenseExpiredError, LicenseUnavailableError, } from './errors/types'; +import { + AllocatableLicenseInfo, + DateWithZeroTime, +} from '../../features/licenses/types/types'; import { NewAllocatedLicenseExpirationDate } from '../../features/licenses/types/types'; @Injectable() @@ -394,7 +398,54 @@ export class LicensesRepositoryService { } }); } + /** + * 対象のアカウントの割り当て可能なライセンスを取得する + * @context Context + * @param accountId + * @param tier + * @return AllocatableLicenseInfo[] + */ + async getAllocatableLicenses( + myAccountId: number, + ): Promise { + const nowDate = new DateWithZeroTime(); + return await this.dataSource.transaction(async (entityManager) => { + const licenseRepo = entityManager.getRepository(License); + const allocatableLicenses = await licenseRepo.find({ + where: [ + { + account_id: myAccountId, + status: In([ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ]), + expiry_date: MoreThanOrEqual(nowDate), + }, + { + account_id: myAccountId, + status: In([ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ]), + expiry_date: IsNull(), + }, + ], + order: { + expiry_date: { + direction: 'DESC', + nulls: 'FIRST', + }, + id: 'ASC', + }, + }); + + return allocatableLicenses.map((license) => ({ + licenseId: license.id, + expiryDate: license.expiry_date, + })); + }); + } /** * ライセンスをユーザーに割り当てる * @param userId