Merged PR 321: API実装(割り当て可能ライセンス取得API)

## 概要
[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."エラーとなることを確認。
・他
①ログがポリシに従って出ていることの確認
## 補足
- 相談、参考資料などがあれば
This commit is contained in:
maruyama.t 2023-08-22 08:55:56 +00:00
parent 417ba17d13
commit 1843844c48
5 changed files with 227 additions and 24 deletions

View File

@ -1934,16 +1934,6 @@
"summary": "", "summary": "",
"description": "割り当て可能なライセンスを取得します", "description": "割り当て可能なライセンスを取得します",
"parameters": [], "parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetAllocatableLicensesRequest"
}
}
}
},
"responses": { "responses": {
"200": { "200": {
"description": "成功時のレスポンス", "description": "成功時のレスポンス",
@ -2850,7 +2840,6 @@
"required": ["cardLicenseKey"] "required": ["cardLicenseKey"]
}, },
"ActivateCardLicensesResponse": { "type": "object", "properties": {} }, "ActivateCardLicensesResponse": { "type": "object", "properties": {} },
"GetAllocatableLicensesRequest": { "type": "object", "properties": {} },
"AllocatableLicenseInfo": { "AllocatableLicenseInfo": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -32,6 +32,7 @@ import { AuthGuard } from '../../common/guards/auth/authguards';
import { RoleGuard } from '../../common/guards/role/roleguards'; import { RoleGuard } from '../../common/guards/role/roleguards';
import { ADMIN_ROLES, TIERS } from '../../constants'; import { ADMIN_ROLES, TIERS } from '../../constants';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { makeContext } from '../../common/log';
@ApiTags('licenses') @ApiTags('licenses')
@Controller('licenses') @Controller('licenses')
@ -201,17 +202,18 @@ export class LicensesController {
async getAllocatableLicenses( async getAllocatableLicenses(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@Req() req: Request, @Req() req: Request,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@Body() body: GetAllocatableLicensesRequest,
): Promise<GetAllocatableLicensesResponse> { ): Promise<GetAllocatableLicensesResponse> {
// TODO 仮の戻り値 const token = retrieveAuthorizationToken(req);
return { const payload = jwt.decode(token, { json: true }) as AccessToken;
allocatableLicenses: [
{ licenseId: 1, expiryDate: null }, const context = makeContext(payload.userId);
{ licenseId: 2, expiryDate: null },
{ licenseId: 3, expiryDate: new Date(2023, 12, 31, 23, 59, 59) }, const allocatableLicenses =
{ licenseId: 4, expiryDate: new Date(2023, 10, 31, 23, 59, 59) }, await this.licensesService.getAllocatableLicenses(
], context,
}; payload.userId,
);
return allocatableLicenses;
} }
} }

View File

@ -359,6 +359,123 @@ describe('DBテスト', () => {
).toBeDefined(); ).toBeDefined();
expect(dbSelectResultFromLicense.license.account_id).toEqual(accountId); 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>(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('ライセンス割り当て', () => { describe('ライセンス割り当て', () => {

View File

@ -11,7 +11,11 @@ import {
} from '../../repositories/licenses/errors/types'; } from '../../repositories/licenses/errors/types';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import { UserNotFoundError } from '../../repositories/users/errors/types'; import { UserNotFoundError } from '../../repositories/users/errors/types';
import { IssueCardLicensesResponse } from './types/types'; import {
GetAllocatableLicensesResponse,
IssueCardLicensesResponse,
} from './types/types';
import { Context } from '../../common/log';
@Injectable() @Injectable()
export class LicensesService { export class LicensesService {
@ -211,4 +215,44 @@ export class LicensesService {
this.logger.log(`[OUT] ${this.activateCardLicenseKey.name}`); this.logger.log(`[OUT] ${this.activateCardLicenseKey.name}`);
return; return;
} }
/**
* get allocatable lisences
* @param context
* @param userId
* @@returns AllocatableLicenseInfo[]
*/
async getAllocatableLicenses(
context: Context,
userId: string,
): Promise<GetAllocatableLicensesResponse> {
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}`,
);
}
}
} }

View File

@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, In } from 'typeorm'; import { DataSource, In, IsNull, MoreThanOrEqual } from 'typeorm';
import { import {
LicenseOrder, LicenseOrder,
License, License,
@ -26,6 +26,10 @@ import {
LicenseExpiredError, LicenseExpiredError,
LicenseUnavailableError, LicenseUnavailableError,
} from './errors/types'; } from './errors/types';
import {
AllocatableLicenseInfo,
DateWithZeroTime,
} from '../../features/licenses/types/types';
import { NewAllocatedLicenseExpirationDate } from '../../features/licenses/types/types'; import { NewAllocatedLicenseExpirationDate } from '../../features/licenses/types/types';
@Injectable() @Injectable()
@ -394,7 +398,54 @@ export class LicensesRepositoryService {
} }
}); });
} }
/**
*
* @context Context
* @param accountId
* @param tier
* @return AllocatableLicenseInfo[]
*/
async getAllocatableLicenses(
myAccountId: number,
): Promise<AllocatableLicenseInfo[]> {
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 * @param userId