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:
parent
c0b99203da
commit
aef30c8cbe
@ -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;
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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()}`,
|
||||
});
|
||||
|
||||
|
||||
@ -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 };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user