Merged PR 748: 第五階層ライセンス情報取得API実装

## 概要
[Task3655: 第五階層ライセンス情報取得API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3655)

- 第五階層ライセンス情報取得APIに、ストレージ上限とストレージ使用量を取得する処理を追加しました。
- 既存の「割り当て済みライセンス取得処理」と「再利用可能ライセンス取得処理」に不要な条件があったため削除しました

## レビューポイント
- 上限計算方法、使用量取得条件に仕様との認識齟齬はないか?
    - もしくはテストケースで「これもあったほうがいいのでは?」などないか
- その他気になる点あれば

## 動作確認状況
- ローカルでテストが全部通ることを確認
This commit is contained in:
Kentaro Fukunaga 2024-02-16 02:11:34 +00:00
parent c0b99203da
commit aef30c8cbe
7 changed files with 249 additions and 48 deletions

View File

@ -327,3 +327,9 @@ export const USER_LICENSE_STATUS = {
* @const {number}
*/
export const FILE_RETENTION_DAYS_DEFAULT = 30;
/**
* 1使GB
* @const {number}
*/
export const STORAGE_SIZE_PER_LICENSE = 5;

View File

@ -13,6 +13,7 @@ import {
} from './test/accounts.service.mock';
import { makeDefaultConfigValue } from '../users/test/users.service.mock';
import {
createAudioFile,
createLicense,
createLicenseOrder,
createLicenseSetExpiryDateAndStatus,
@ -47,6 +48,7 @@ import {
LICENSE_ISSUE_STATUS,
LICENSE_TYPE,
OPTION_ITEM_VALUE_TYPE,
STORAGE_SIZE_PER_LICENSE,
TASK_STATUS,
TIERS,
USER_ROLES,
@ -1912,7 +1914,7 @@ describe('getLicenseSummary', () => {
await createLicenseSetExpiryDateAndStatus(
source,
childAccountId1,
null,
new Date(2037, 1, 1, 23, 59, 59),
'Allocated',
1,
);
@ -1937,7 +1939,7 @@ describe('getLicenseSummary', () => {
expiringWithin14daysLicense: 5,
issueRequesting: 100,
numberOfRequesting: 1,
storageSize: 0,
storageSize: 40000000000,
usedSize: 0,
shortage: 2,
isStorageAvailable: false,
@ -1950,12 +1952,135 @@ describe('getLicenseSummary', () => {
expiringWithin14daysLicense: 5,
issueRequesting: 0,
numberOfRequesting: 0,
storageSize: 25000000000,
usedSize: 0,
shortage: 0,
isStorageAvailable: false,
});
});
it('第五階層のストレージ使用量が取得できる(ライセンスなし、音声ファイルなし)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// アカウントを作成する
const { id: accountId } = (
await makeTestAccount(source, {
tier: 5,
company_name: 'company1',
})
).account;
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId');
const result = await service.getLicenseSummary(context, accountId);
expect(result).toEqual({
totalLicense: 0,
allocatedLicense: 0,
reusableLicense: 0,
freeLicense: 0,
expiringWithin14daysLicense: 0,
issueRequesting: 0,
numberOfRequesting: 0,
storageSize: 0,
usedSize: 0,
shortage: 0,
isStorageAvailable: false,
});
});
it('第五階層のストレージ使用量が取得できる(ライセンスあり、音声ファイルあり)', async () => {
if (!source) fail();
const module = await makeTestingModule(source);
if (!module) fail();
// アカウントを作成する
const { id: accountId } = (
await makeTestAccount(source, {
tier: 5,
company_name: 'company1',
})
).account;
// audioFileを作成する
const fileSize1 = 15000;
await createAudioFile(source, accountId, 1, fileSize1);
const fileSize2 = 17000;
await createAudioFile(source, accountId, 1, fileSize2);
// ライセンスを作成する
const reusableLicense = 3;
for (let i = 0; i < reusableLicense; i++) {
await createLicenseSetExpiryDateAndStatus(
source,
accountId,
new Date(2037, 1, 1, 23, 59, 59),
LICENSE_ALLOCATED_STATUS.REUSABLE,
);
}
const allocatedLicense = 2;
for (let i = 0; i < allocatedLicense; i++) {
await createLicenseSetExpiryDateAndStatus(
source,
accountId,
new Date(2037, 1, 1, 23, 59, 59),
LICENSE_ALLOCATED_STATUS.ALLOCATED,
i + 1, // なんでもよい。重複しないようにインクリメントする。
);
}
const unallocatedLicense = 5;
for (let i = 0; i < unallocatedLicense; i++) {
await createLicenseSetExpiryDateAndStatus(
source,
accountId,
null,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
);
}
// 自アカウントだけに絞って計算出来ていることを確認するため、別のアカウントとaudioFileとライセンス作成する。
const { id: otherAccountId } = (
await makeTestAccount(source, {
tier: 5,
company_name: 'company2',
})
).account;
await createAudioFile(source, otherAccountId, 1, 5000);
await createLicenseSetExpiryDateAndStatus(
source,
otherAccountId,
new Date(2037, 1, 1, 23, 59, 59),
LICENSE_ALLOCATED_STATUS.ALLOCATED,
);
// テスト実行
const service = module.get<AccountsService>(AccountsService);
const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId');
const result = await service.getLicenseSummary(context, accountId);
const expectedStorageSize =
(reusableLicense + allocatedLicense) *
STORAGE_SIZE_PER_LICENSE *
1000 *
1000 *
1000; // 5GB
expect(result).toEqual({
totalLicense: reusableLicense + unallocatedLicense,
allocatedLicense: allocatedLicense,
reusableLicense: reusableLicense,
freeLicense: unallocatedLicense,
expiringWithin14daysLicense: 0,
issueRequesting: 0,
numberOfRequesting: 0,
storageSize: expectedStorageSize,
usedSize: fileSize1 + fileSize2,
shortage: 0,
isStorageAvailable: false,
});
});
});
describe('getPartnerAccount', () => {

View File

@ -131,6 +131,12 @@ export class AccountsService {
let shortage = allocatableLicenseWithMargin - expiringSoonLicense;
shortage = shortage >= 0 ? 0 : Math.abs(shortage);
const { size, used } = await this.licensesRepository.getStorageInfo(
context,
accountId,
currentDate,
);
const licenseSummaryResponse: GetLicenseSummaryResponse = {
totalLicense,
allocatedLicense,
@ -139,8 +145,8 @@ export class AccountsService {
expiringWithin14daysLicense: expiringSoonLicense,
issueRequesting,
numberOfRequesting,
storageSize: 0, // XXX PBI1201対象外
usedSize: 0, // XXX PBI1201対象外
storageSize: size,
usedSize: used,
shortage,
isStorageAvailable,
};

View File

@ -10,6 +10,7 @@ import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity
import { OptionItem } from '../../../repositories/worktypes/entity/option_item.entity';
import { OPTION_ITEM_VALUE_TYPE } from '../../../constants';
import { Account } from '../../../repositories/accounts/entity/account.entity';
import { AudioFile } from '../../../repositories/audio_files/entity/audio_file.entity';
/**
* ユーティリティ: すべてのソート条件を取得する
@ -219,3 +220,31 @@ export const getOptionItems = async (
})
: await datasource.getRepository(OptionItem).find();
};
export const createAudioFile = async (
datasource: DataSource,
account_id: number,
owner_user_id: number,
fileSize: number,
): Promise<{ audioFileId: number }> => {
const { identifiers: audioFileIdentifiers } = await datasource
.getRepository(AudioFile)
.insert({
account_id: account_id,
owner_user_id: owner_user_id,
url: '',
file_name: 'x.zip',
author_id: 'author_id',
work_type_id: '',
started_at: new Date(),
duration: '100000',
finished_at: new Date(),
uploaded_at: new Date(),
file_size: fileSize,
priority: '00',
audio_format: 'audio_format',
is_encrypted: true,
});
const audioFile = audioFileIdentifiers.pop() as AudioFile;
return { audioFileId: audioFile.id };
};

View File

@ -4120,23 +4120,23 @@ describe('getNextTask', () => {
describe('deleteTask', () => {
let source: DataSource | null = null;
beforeAll(async () => {
if (source == null) {
source = await (async () => {
const s = new DataSource({
type: 'mysql',
host: 'test_mysql_db',
port: 3306,
username: 'user',
password: 'password',
database: 'odms',
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
});
return await s.initialize();
})();
}
});
beforeAll(async () => {
if (source == null) {
source = await (async () => {
const s = new DataSource({
type: 'mysql',
host: 'test_mysql_db',
port: 3306,
username: 'user',
password: 'password',
database: 'odms',
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
});
return await s.initialize();
})();
}
});
beforeEach(async () => {
if (source) {

View File

@ -397,37 +397,21 @@ export class AccountsRepositoryService {
// 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する
const allocatedLicense = await license.count({
where: [
{
account_id: id,
allocated_user_id: Not(IsNull()),
expiry_date: MoreThanOrEqual(currentDate),
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
},
{
account_id: id,
allocated_user_id: Not(IsNull()),
expiry_date: IsNull(),
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
},
],
where: {
account_id: id,
expiry_date: MoreThanOrEqual(currentDate),
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
// 総ライセンス数のうち、ユーザーに割り当てたことがあるが、現在は割り当て解除され誰にも割り当たっていないライセンス数を取得する
const reusableLicense = await license.count({
where: [
{
account_id: id,
expiry_date: MoreThanOrEqual(currentDate),
status: LICENSE_ALLOCATED_STATUS.REUSABLE,
},
{
account_id: id,
expiry_date: IsNull(),
status: LICENSE_ALLOCATED_STATUS.REUSABLE,
},
],
where: {
account_id: id,
expiry_date: MoreThanOrEqual(currentDate),
status: LICENSE_ALLOCATED_STATUS.REUSABLE,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});

View File

@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { DataSource, In, IsNull, MoreThanOrEqual, Not } from 'typeorm';
import {
LicenseOrder,
License,
@ -12,6 +12,7 @@ import {
LICENSE_ALLOCATED_STATUS,
LICENSE_ISSUE_STATUS,
LICENSE_TYPE,
STORAGE_SIZE_PER_LICENSE,
SWITCH_FROM_TYPE,
TIERS,
USER_LICENSE_STATUS,
@ -41,6 +42,7 @@ import {
import { Context } from '../../common/log';
import { User } from '../users/entity/user.entity';
import { UserNotFoundError } from '../users/errors/types';
import { AudioFile } from '../audio_files/entity/audio_file.entity';
@Injectable()
export class LicensesRepositoryService {
@ -862,4 +864,53 @@ export class LicensesRepositoryService {
return { state: USER_LICENSE_STATUS.ALLOCATED };
}
/**
* 使
* @param context
* @param accountId
* @param currentDate
* @returns size: ストレージ上限, used: 使用量
*/
async getStorageInfo(
context: Context,
accountId: number,
currentDate: Date,
): Promise<{ size: number; used: number }> {
return await this.dataSource.transaction(async (entityManager) => {
// ストレージ上限計算のための値を取得する。(ユーザーに一度でも割り当てたことのあるライセンス数)
const licenseRepo = entityManager.getRepository(License);
const licensesAllocatedOnce = await licenseRepo.count({
where: {
account_id: accountId,
expiry_date: MoreThanOrEqual(currentDate),
status: In([
LICENSE_ALLOCATED_STATUS.ALLOCATED,
LICENSE_ALLOCATED_STATUS.REUSABLE,
]),
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
// ストレージ上限を計算する
const size =
licensesAllocatedOnce * STORAGE_SIZE_PER_LICENSE * 1000 * 1000 * 1000; // GB -> B
// 既に使用しているストレージ量を取得する
const audioFileRepo = entityManager.getRepository(AudioFile);
const usedQuery = await audioFileRepo
.createQueryBuilder('audioFile')
.select('SUM(audioFile.file_size)', 'used')
.where('audioFile.account_id = :accountId', { accountId })
.comment(`${context.getTrackingId()}_${new Date().toUTCString()}`)
.getRawOne();
let used = parseInt(usedQuery?.used);
if (isNaN(used)) {
// AudioFileのレコードが存在しない場合、SUM関数がNULLを返すため、0を返す
used = 0;
}
return { size, used };
});
}
}