diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index 979f864..cc23b64 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -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; diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 5334092..af5ac04 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -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); + 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); + 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', () => { diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index bad5d49..28ee33d 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -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, }; diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index b68b5fd..ff0e436 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -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 }; +}; diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 54e191b..27ffcfe 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -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) { diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index ee4fc2d..4a516a8 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -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()}`, }); diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 6270b43..5d20eb4 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, 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 }; + }); + } }