Revert "Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース" Reverted commit `b5293888`. デプロイミスによる切り戻し
925 lines
31 KiB
TypeScript
925 lines
31 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
||
import { DataSource, In, IsNull, MoreThanOrEqual, Not } from 'typeorm';
|
||
import {
|
||
LicenseOrder,
|
||
License,
|
||
CardLicenseIssue,
|
||
CardLicense,
|
||
LicenseAllocationHistory,
|
||
} from './entity/license.entity';
|
||
import {
|
||
CARD_LICENSE_LENGTH,
|
||
LICENSE_ALLOCATED_STATUS,
|
||
LICENSE_ISSUE_STATUS,
|
||
LICENSE_TYPE,
|
||
STORAGE_SIZE_PER_LICENSE,
|
||
SWITCH_FROM_TYPE,
|
||
TIERS,
|
||
USER_LICENSE_STATUS,
|
||
} from '../../constants';
|
||
import {
|
||
PoNumberAlreadyExistError,
|
||
LicenseNotExistError,
|
||
LicenseKeyAlreadyActivatedError,
|
||
LicensesShortageError,
|
||
AlreadyIssuedError,
|
||
OrderNotFoundError,
|
||
LicenseExpiredError,
|
||
LicenseUnavailableError,
|
||
LicenseAlreadyDeallocatedError,
|
||
CancelOrderFailedError,
|
||
} from './errors/types';
|
||
import {
|
||
AllocatableLicenseInfo,
|
||
DateWithZeroTime,
|
||
} from '../../features/licenses/types/types';
|
||
import { NewAllocatedLicenseExpirationDate } from '../../features/licenses/types/types';
|
||
import {
|
||
insertEntity,
|
||
insertEntities,
|
||
updateEntity,
|
||
} from '../../common/repository';
|
||
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 {
|
||
//クエリログにコメントを出力するかどうか
|
||
private readonly isCommentOut = process.env.STAGE !== 'local';
|
||
constructor(private dataSource: DataSource) {}
|
||
private readonly logger = new Logger(LicensesRepositoryService.name);
|
||
|
||
async order(
|
||
context: Context,
|
||
poNumber: string,
|
||
fromAccountId: number,
|
||
toAccountId: number,
|
||
quantity: number,
|
||
): Promise<LicenseOrder> {
|
||
const licenseOrder = new LicenseOrder();
|
||
licenseOrder.po_number = poNumber;
|
||
licenseOrder.from_account_id = fromAccountId;
|
||
licenseOrder.to_account_id = toAccountId;
|
||
licenseOrder.quantity = quantity;
|
||
licenseOrder.status = LICENSE_ISSUE_STATUS.ISSUE_REQUESTING;
|
||
|
||
// ライセンス注文テーブルに登録する
|
||
const createdEntity = await this.dataSource.transaction(
|
||
async (entityManager) => {
|
||
//poNumberの重複チェックを行う
|
||
const isPoNumberDuplicated = await entityManager
|
||
.getRepository(LicenseOrder)
|
||
.findOne({
|
||
where: [
|
||
{
|
||
po_number: poNumber,
|
||
from_account_id: fromAccountId,
|
||
status: LICENSE_ISSUE_STATUS.ISSUED,
|
||
},
|
||
{
|
||
po_number: poNumber,
|
||
from_account_id: fromAccountId,
|
||
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
|
||
},
|
||
],
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
lock: { mode: 'pessimistic_write' },
|
||
});
|
||
// 重複があった場合はエラーを返却する
|
||
if (isPoNumberDuplicated) {
|
||
throw new PoNumberAlreadyExistError(`This PoNumber already used.`);
|
||
}
|
||
|
||
const repo = entityManager.getRepository(LicenseOrder);
|
||
const newLicenseOrder = repo.create(licenseOrder);
|
||
const persisted = await insertEntity(
|
||
LicenseOrder,
|
||
repo,
|
||
newLicenseOrder,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
return persisted;
|
||
},
|
||
);
|
||
return createdEntity;
|
||
}
|
||
|
||
/**
|
||
* オーダーIDとPO番号と依頼元アカウントIDを元にライセンス注文を取得する
|
||
* @param context context
|
||
* @param fromAccountId ライセンス注文を行ったアカウントのID
|
||
* @param poNumber PO番号
|
||
* @param orderId LicenseOrderのID
|
||
* @returns license order
|
||
*/
|
||
async getLicenseOrder(
|
||
context: Context,
|
||
fromAccountId: number,
|
||
poNumber: string,
|
||
orderId: number,
|
||
): Promise<LicenseOrder | null> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const repo = entityManager.getRepository(LicenseOrder);
|
||
|
||
// ステータスは問わず、指定したIDのランセンス注文を取得する
|
||
const entity = repo.findOne({
|
||
where: {
|
||
id: orderId,
|
||
po_number: poNumber,
|
||
from_account_id: fromAccountId,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
return entity;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* カードライセンスを発行する
|
||
* @param accountId
|
||
* @param count
|
||
* @returns string[] カードライセンスキーの配列
|
||
*/
|
||
async createCardLicenses(
|
||
context: Context,
|
||
accountId: number,
|
||
count: number,
|
||
): Promise<string[]> {
|
||
const licenseKeys: string[] = [];
|
||
|
||
await this.dataSource.transaction(async (entityManager) => {
|
||
const licensesRepo = entityManager.getRepository(License);
|
||
const cardLicenseRepo = entityManager.getRepository(CardLicense);
|
||
const cardLicenseIssueRepo =
|
||
entityManager.getRepository(CardLicenseIssue);
|
||
|
||
const licenses: License[] = [];
|
||
// ライセンステーブルを作成する(BULK INSERT)
|
||
for (let i = 0; i < count; i++) {
|
||
const license = new License();
|
||
license.account_id = accountId;
|
||
license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
|
||
license.type = LICENSE_TYPE.CARD;
|
||
licenses.push(license);
|
||
}
|
||
const savedLicenses = await insertEntities(
|
||
License,
|
||
licensesRepo,
|
||
licenses,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
// カードライセンス発行テーブルを作成する
|
||
const cardLicenseIssue = new CardLicenseIssue();
|
||
cardLicenseIssue.issued_at = new Date();
|
||
const newCardLicenseIssue = cardLicenseIssueRepo.create(cardLicenseIssue);
|
||
const savedCardLicensesIssue = await insertEntity(
|
||
CardLicenseIssue,
|
||
cardLicenseIssueRepo,
|
||
newCardLicenseIssue,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
let isDuplicateKeysExist = true;
|
||
let generateCount = count;
|
||
while (isDuplicateKeysExist) {
|
||
const generateKeys = await this.generateLicenseKeys(
|
||
generateCount,
|
||
licenseKeys,
|
||
);
|
||
// licenseKeysが既にカードライセンステーブルに存在するかチェック
|
||
const existingCardLicenses = await cardLicenseRepo.find({
|
||
where: {
|
||
card_license_key: In(generateKeys),
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
lock: { mode: 'pessimistic_write' },
|
||
});
|
||
if (existingCardLicenses.length > 0) {
|
||
// 重複分を配列から削除
|
||
existingCardLicenses.forEach((existKey) => {
|
||
generateKeys.splice(
|
||
generateKeys.indexOf(existKey.card_license_key),
|
||
1,
|
||
);
|
||
});
|
||
// 重複がなかったものを格納
|
||
generateKeys.forEach((keys) => {
|
||
licenseKeys.push(keys);
|
||
});
|
||
// 重複分の再生成を行う
|
||
generateCount = existingCardLicenses.length;
|
||
continue;
|
||
}
|
||
// 重複がない場合は本ループで作成したkeyをすべて格納
|
||
generateKeys.forEach((keys) => {
|
||
licenseKeys.push(keys);
|
||
});
|
||
// 重複がない場合はループを終了
|
||
isDuplicateKeysExist = false;
|
||
}
|
||
|
||
const cardLicenses: CardLicense[] = [];
|
||
// カードライセンステーブルを作成する(BULK INSERT)
|
||
for (let i = 0; i < count; i++) {
|
||
const cardLicense = new CardLicense();
|
||
cardLicense.license_id = savedLicenses[i].id; // Licenseテーブルの自動採番されたIDを挿入
|
||
cardLicense.issue_id = savedCardLicensesIssue.id; // CardLicenseIssueテーブルの自動採番されたIDを挿入
|
||
cardLicense.card_license_key = licenseKeys[i];
|
||
cardLicenses.push(cardLicense);
|
||
}
|
||
//TODO カードライセンステーブルのみPKがidではなかったためInsertEntitiesに置き換えができなかった。
|
||
const query = cardLicenseRepo
|
||
.createQueryBuilder()
|
||
.insert()
|
||
.into(CardLicense);
|
||
if (this.isCommentOut) {
|
||
query.comment(`${context.getTrackingId()}_${new Date().toUTCString()}`);
|
||
}
|
||
query.values(cardLicenses).execute();
|
||
});
|
||
return licenseKeys;
|
||
}
|
||
|
||
/**
|
||
* ランダム(大文字英数字)の一意のライセンスキーを作成する。
|
||
* @param count
|
||
* @param existingLicenseKeys 既に作成されたライセンスキーの配列
|
||
* @returns licenseKeys
|
||
*/
|
||
async generateLicenseKeys(
|
||
count: number,
|
||
existingLicenseKeys: string[],
|
||
): Promise<string[]> {
|
||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||
const licenseKeys: string[] = [];
|
||
|
||
while (licenseKeys.length < count) {
|
||
let licenseKey = '';
|
||
for (let i = 0; i < CARD_LICENSE_LENGTH; i++) {
|
||
const randomIndex = Math.floor(Math.random() * characters.length);
|
||
licenseKey += characters[randomIndex];
|
||
}
|
||
|
||
// 重複しない一意のライセンスキーを生成するまで繰り返す
|
||
if (
|
||
!licenseKeys.includes(licenseKey) &&
|
||
!existingLicenseKeys.includes(licenseKey)
|
||
) {
|
||
licenseKeys.push(licenseKey);
|
||
}
|
||
}
|
||
|
||
return licenseKeys;
|
||
}
|
||
|
||
/**
|
||
* カードライセンスを取り込む
|
||
* @param accountId
|
||
* @param licenseKey
|
||
* @returns void
|
||
*/
|
||
async activateCardLicense(
|
||
context: Context,
|
||
accountId: number,
|
||
licenseKey: string,
|
||
): Promise<void> {
|
||
await this.dataSource.transaction(async (entityManager) => {
|
||
const cardLicenseRepo = entityManager.getRepository(CardLicense);
|
||
|
||
// カードライセンステーブルを検索
|
||
const targetCardLicense = await cardLicenseRepo.findOne({
|
||
where: {
|
||
card_license_key: licenseKey,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
lock: { mode: 'pessimistic_write' },
|
||
});
|
||
// カードライセンスが存在しなければエラー
|
||
if (!targetCardLicense) {
|
||
this.logger.error(
|
||
`card license key not exist. card_licence_key: ${licenseKey}`,
|
||
);
|
||
throw new LicenseNotExistError(`License not exist.`);
|
||
}
|
||
// 既に取り込み済みならエラー
|
||
if (targetCardLicense.activated_at) {
|
||
this.logger.error(
|
||
`card license already activated. card_licence_key: ${licenseKey}`,
|
||
);
|
||
throw new LicenseKeyAlreadyActivatedError(`License already activated.`);
|
||
}
|
||
|
||
const licensesRepo = entityManager.getRepository(License);
|
||
|
||
// ライセンステーブルを検索
|
||
const targetLicense = await licensesRepo.findOne({
|
||
where: {
|
||
id: targetCardLicense.license_id,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
// ライセンスが存在しなければエラー
|
||
if (!targetLicense) {
|
||
this.logger.error(
|
||
`license not exist. licence_id: ${targetCardLicense.license_id}`,
|
||
);
|
||
throw new LicenseNotExistError(`License not exist.`);
|
||
}
|
||
|
||
// ライセンステーブルを更新する
|
||
targetLicense.account_id = accountId;
|
||
await updateEntity(
|
||
licensesRepo,
|
||
{ id: targetLicense.id },
|
||
targetLicense,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
// カードライセンステーブルを更新する
|
||
targetCardLicense.activated_at = new Date();
|
||
await updateEntity(
|
||
cardLicenseRepo,
|
||
{ license_id: targetCardLicense.license_id },
|
||
targetCardLicense,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
this.logger.log(
|
||
`activate success. licence_id: ${targetCardLicense.license_id}`,
|
||
);
|
||
});
|
||
return;
|
||
}
|
||
|
||
/**
|
||
* アカウントIDに紐づく注文履歴情報を取得する
|
||
* @param accountId
|
||
* @param offset
|
||
* @param limit
|
||
* @returns total
|
||
* @returns licenseOrders
|
||
*/
|
||
async getLicenseOrderHistoryInfo(
|
||
context: Context,
|
||
accountId: number,
|
||
offset: number,
|
||
limit: number,
|
||
): Promise<{
|
||
total: number;
|
||
licenseOrders: LicenseOrder[];
|
||
}> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const licenseOrder = entityManager.getRepository(LicenseOrder);
|
||
// limit/offsetによらない総件数を取得する
|
||
const total = await licenseOrder.count({
|
||
where: {
|
||
from_account_id: accountId,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
const licenseOrders = await licenseOrder.find({
|
||
where: {
|
||
from_account_id: accountId,
|
||
},
|
||
order: {
|
||
ordered_at: 'DESC',
|
||
},
|
||
take: limit,
|
||
skip: offset,
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
return {
|
||
total: total,
|
||
licenseOrders: licenseOrders,
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 対象の注文を発行する
|
||
* @context Context
|
||
* @param orderedAccountId
|
||
* @param myAccountId
|
||
* @param tier
|
||
* @param poNumber
|
||
*/
|
||
async issueLicense(
|
||
context: Context,
|
||
orderedAccountId: number,
|
||
myAccountId: number,
|
||
tier: number,
|
||
poNumber: string,
|
||
): Promise<{ issuedOrderId: number }> {
|
||
const nowDate = new Date();
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const licenseOrderRepo = entityManager.getRepository(LicenseOrder);
|
||
const licenseRepo = entityManager.getRepository(License);
|
||
|
||
const issuingOrder = await licenseOrderRepo.findOne({
|
||
where: {
|
||
from_account_id: orderedAccountId,
|
||
po_number: poNumber,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
lock: { mode: 'pessimistic_write' },
|
||
});
|
||
if (!issuingOrder) {
|
||
// 注文が存在しない場合、エラー
|
||
throw new OrderNotFoundError(`No order found for PONumber:${poNumber}`);
|
||
}
|
||
// 既に発行済みの注文の場合、エラー
|
||
if (issuingOrder.status !== LICENSE_ISSUE_STATUS.ISSUE_REQUESTING) {
|
||
throw new AlreadyIssuedError(
|
||
`An order for PONumber:${poNumber} has already been issued.`,
|
||
);
|
||
}
|
||
|
||
// ライセンステーブルのレコードを作成する
|
||
const newLicenses = Array.from({ length: issuingOrder.quantity }, () => {
|
||
const license = new License();
|
||
license.account_id = orderedAccountId;
|
||
license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
|
||
license.type = LICENSE_TYPE.NORMAL;
|
||
license.order_id = issuingOrder.id;
|
||
return license;
|
||
});
|
||
// ライセンス注文テーブルを更新(注文元)
|
||
await updateEntity(
|
||
licenseOrderRepo,
|
||
{ id: issuingOrder.id },
|
||
{
|
||
issued_at: nowDate,
|
||
status: LICENSE_ISSUE_STATUS.ISSUED,
|
||
},
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
// ライセンステーブルを登録(注文元)
|
||
await insertEntities(
|
||
License,
|
||
licenseRepo,
|
||
newLicenses,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
// 第一階層の場合はストックライセンスの概念が存在しないため、ストックライセンス変更処理は行わない
|
||
if (tier !== TIERS.TIER1) {
|
||
const licensesToUpdate = await licenseRepo.find({
|
||
where: {
|
||
account_id: myAccountId,
|
||
status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||
type: LICENSE_TYPE.NORMAL,
|
||
},
|
||
order: {
|
||
id: 'ASC',
|
||
},
|
||
take: newLicenses.length,
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
// 登録したライセンスに対して自身のライセンスが不足していた場合、エラー
|
||
if (newLicenses.length > licensesToUpdate.length) {
|
||
throw new LicensesShortageError(
|
||
`Shortage Licenses.Number of licenses attempted to be issued is ${newLicenses.length}.`,
|
||
);
|
||
}
|
||
for (const licenseToUpdate of licensesToUpdate) {
|
||
licenseToUpdate.status = LICENSE_ALLOCATED_STATUS.DELETED;
|
||
licenseToUpdate.deleted_at = nowDate;
|
||
licenseToUpdate.delete_order_id = issuingOrder.id;
|
||
}
|
||
// 自身のライセンスを削除(論理削除)する
|
||
await updateEntity(
|
||
licenseRepo,
|
||
{ id: In(licensesToUpdate.map((l) => l.id)) },
|
||
{
|
||
status: LICENSE_ALLOCATED_STATUS.DELETED,
|
||
deleted_at: nowDate,
|
||
delete_order_id: issuingOrder.id,
|
||
},
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
}
|
||
|
||
return { issuedOrderId: issuingOrder.id };
|
||
});
|
||
}
|
||
/**
|
||
* 対象のアカウントの割り当て可能なライセンスを取得する
|
||
* @context Context
|
||
* @param accountId
|
||
* @param tier
|
||
* @return AllocatableLicenseInfo[]
|
||
*/
|
||
async getAllocatableLicenses(
|
||
context: Context,
|
||
myAccountId: number,
|
||
): Promise<AllocatableLicenseInfo[]> {
|
||
const licenseRepo = this.dataSource.getRepository(License);
|
||
// EntityManagerではorderBy句で、expiry_dateに対して複数条件でソートを使用するため出来ない為、createQueryBuilderを使用する。
|
||
// プロダクト バックログ項目 4218: [FB対応]有効期限当日のライセンスは一覧に表示しない の対応
|
||
// 有効期限が当日のライセンスは取得しない
|
||
// 明日の00:00:00を取得
|
||
const tomorrowDate = new DateWithZeroTime(
|
||
new Date().setDate(new Date().getDate() + 1),
|
||
);
|
||
const queryBuilder = licenseRepo
|
||
.createQueryBuilder('license')
|
||
.where('license.account_id = :accountId', { accountId: myAccountId })
|
||
.andWhere('license.status IN (:...statuses)', {
|
||
statuses: [
|
||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||
LICENSE_ALLOCATED_STATUS.REUSABLE,
|
||
],
|
||
})
|
||
.andWhere(
|
||
'(license.expiry_date >= :tomorrowDate OR license.expiry_date IS NULL)',
|
||
{ tomorrowDate },
|
||
)
|
||
.comment(`${context.getTrackingId()}_${new Date().toUTCString()}`)
|
||
.orderBy('license.expiry_date IS NULL', 'DESC')
|
||
.addOrderBy('license.expiry_date', 'DESC')
|
||
.addOrderBy('license.id', 'ASC');
|
||
const allocatableLicenses = await queryBuilder.getMany();
|
||
return allocatableLicenses.map((license) => ({
|
||
licenseId: license.id,
|
||
expiryDate: license.expiry_date ?? undefined,
|
||
}));
|
||
}
|
||
/**
|
||
* ライセンスをユーザーに割り当てる
|
||
* @param userId
|
||
* @param newLicenseId
|
||
*/
|
||
async allocateLicense(
|
||
context: Context,
|
||
userId: number,
|
||
newLicenseId: number,
|
||
accountId: number,
|
||
): Promise<void> {
|
||
await this.dataSource.transaction(async (entityManager) => {
|
||
// 対象ユーザの存在チェック
|
||
const userRepo = entityManager.getRepository(User);
|
||
const user = await userRepo.findOne({
|
||
where: {
|
||
id: userId,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
lock: { mode: 'pessimistic_write' },
|
||
});
|
||
if (!user) {
|
||
throw new UserNotFoundError(`User not exist. userId: ${userId}`);
|
||
}
|
||
|
||
const licenseRepo = entityManager.getRepository(License);
|
||
const licenseAllocationHistoryRepo = entityManager.getRepository(
|
||
LicenseAllocationHistory,
|
||
);
|
||
// 割り当て対象のライセンス情報を取得
|
||
const targetLicense = await licenseRepo.findOne({
|
||
where: {
|
||
id: newLicenseId,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
lock: { mode: 'pessimistic_write' },
|
||
});
|
||
|
||
// ライセンスが存在しない場合はエラー
|
||
if (!targetLicense) {
|
||
throw new LicenseNotExistError(
|
||
`License not exist. licenseId: ${newLicenseId}`,
|
||
);
|
||
}
|
||
|
||
// 期限切れの場合はエラー
|
||
// 有効期限が当日のライセンスは割り当て不可
|
||
// XXX 記述は「有効期限が過去のライセンスは割り当て不可」のような意図だと思われるが、実際の処理は「有効期限が当日のライセンスは割り当て不可」になっている
|
||
// より正確な記述に修正したほうが良いが、リリース後のため、修正は保留(2024年6月7日)
|
||
if (targetLicense.expiry_date) {
|
||
const currentDay = new Date();
|
||
currentDay.setHours(23, 59, 59, 999);
|
||
if (targetLicense.expiry_date < currentDay) {
|
||
throw new LicenseExpiredError(
|
||
`License is expired. expiration date: ${targetLicense.expiry_date} current Date: ${currentDay}`,
|
||
);
|
||
}
|
||
}
|
||
// ライセンス状態が「未割当」「再利用可能」以外の場合はエラー
|
||
if (
|
||
targetLicense.status === LICENSE_ALLOCATED_STATUS.ALLOCATED ||
|
||
targetLicense.status === LICENSE_ALLOCATED_STATUS.DELETED
|
||
) {
|
||
throw new LicenseUnavailableError(
|
||
`License is unavailable. License status: ${targetLicense.status}`,
|
||
);
|
||
}
|
||
|
||
// 対象ユーザーのライセンス割り当て状態を取得
|
||
const allocatedLicense = await licenseRepo.findOne({
|
||
where: {
|
||
allocated_user_id: userId,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
lock: { mode: 'pessimistic_write' },
|
||
});
|
||
|
||
// 既にライセンスが割り当てられているなら、割り当てを解除
|
||
if (allocatedLicense) {
|
||
allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE;
|
||
allocatedLicense.allocated_user_id = null;
|
||
|
||
await updateEntity(
|
||
licenseRepo,
|
||
{ id: allocatedLicense.id },
|
||
allocatedLicense,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
// ライセンス割り当て履歴テーブルへ登録
|
||
const deallocationHistory = new LicenseAllocationHistory();
|
||
deallocationHistory.user_id = userId;
|
||
deallocationHistory.license_id = allocatedLicense.id;
|
||
deallocationHistory.account_id = accountId;
|
||
deallocationHistory.is_allocated = false;
|
||
deallocationHistory.executed_at = new Date();
|
||
deallocationHistory.switch_from_type = SWITCH_FROM_TYPE.NONE;
|
||
await insertEntity(
|
||
LicenseAllocationHistory,
|
||
licenseAllocationHistoryRepo,
|
||
deallocationHistory,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
}
|
||
|
||
// ライセンス割り当てを実施
|
||
targetLicense.status = LICENSE_ALLOCATED_STATUS.ALLOCATED;
|
||
targetLicense.allocated_user_id = userId;
|
||
// 有効期限が未設定なら365日後に設定
|
||
if (!targetLicense.expiry_date) {
|
||
targetLicense.expiry_date = new NewAllocatedLicenseExpirationDate();
|
||
}
|
||
await updateEntity(
|
||
licenseRepo,
|
||
{ id: targetLicense.id },
|
||
targetLicense,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
// 直近割り当てたライセンス種別を取得
|
||
const oldLicenseType = await licenseAllocationHistoryRepo.findOne({
|
||
relations: {
|
||
license: true,
|
||
},
|
||
where: { user_id: userId, is_allocated: true },
|
||
order: { executed_at: 'DESC' },
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
let switchFromType = '';
|
||
if (oldLicenseType && oldLicenseType.license) {
|
||
switch (oldLicenseType.license.type) {
|
||
case LICENSE_TYPE.CARD:
|
||
switchFromType = SWITCH_FROM_TYPE.CARD;
|
||
break;
|
||
case LICENSE_TYPE.TRIAL:
|
||
switchFromType = SWITCH_FROM_TYPE.TRIAL;
|
||
break;
|
||
default:
|
||
switchFromType = SWITCH_FROM_TYPE.NONE;
|
||
break;
|
||
}
|
||
} else {
|
||
switchFromType = SWITCH_FROM_TYPE.NONE;
|
||
}
|
||
|
||
// ライセンス割り当て履歴テーブルへ登録
|
||
const allocationHistory = new LicenseAllocationHistory();
|
||
allocationHistory.user_id = userId;
|
||
allocationHistory.license_id = targetLicense.id;
|
||
allocationHistory.account_id = accountId;
|
||
allocationHistory.is_allocated = true;
|
||
allocationHistory.executed_at = new Date();
|
||
// TODO switchFromTypeの値については「PBI1234: 第一階層として、ライセンス数推移情報をCSV出力したい」で正式対応
|
||
allocationHistory.switch_from_type = switchFromType;
|
||
|
||
await insertEntity(
|
||
LicenseAllocationHistory,
|
||
licenseAllocationHistoryRepo,
|
||
allocationHistory,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* ユーザーに割り当てられているライセンスを解除する
|
||
* @param userId
|
||
*/
|
||
async deallocateLicense(
|
||
context: Context,
|
||
userId: number,
|
||
accountId: number,
|
||
): Promise<void> {
|
||
await this.dataSource.transaction(async (entityManager) => {
|
||
const licenseRepo = entityManager.getRepository(License);
|
||
const licenseAllocationHistoryRepo = entityManager.getRepository(
|
||
LicenseAllocationHistory,
|
||
);
|
||
// 対象ユーザーのライセンス割り当て状態を取得
|
||
const allocatedLicense = await licenseRepo.findOne({
|
||
where: {
|
||
allocated_user_id: userId,
|
||
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
lock: { mode: 'pessimistic_write' },
|
||
});
|
||
|
||
// ライセンスが割り当てられていない場合はエラー
|
||
if (!allocatedLicense) {
|
||
throw new LicenseAlreadyDeallocatedError(
|
||
`License is already deallocated. userId: ${userId}`,
|
||
);
|
||
}
|
||
|
||
// ライセンスの割り当てを解除
|
||
allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE;
|
||
allocatedLicense.allocated_user_id = null;
|
||
await updateEntity(
|
||
licenseRepo,
|
||
{ id: allocatedLicense.id },
|
||
allocatedLicense,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
// ライセンス割り当て履歴テーブルへ登録
|
||
const deallocationHistory = new LicenseAllocationHistory();
|
||
deallocationHistory.user_id = userId;
|
||
deallocationHistory.license_id = allocatedLicense.id;
|
||
deallocationHistory.account_id = accountId;
|
||
deallocationHistory.is_allocated = false;
|
||
deallocationHistory.executed_at = new Date();
|
||
deallocationHistory.switch_from_type = SWITCH_FROM_TYPE.NONE;
|
||
await insertEntity(
|
||
LicenseAllocationHistory,
|
||
licenseAllocationHistoryRepo,
|
||
deallocationHistory,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* ライセンス注文をキャンセルする
|
||
* @param accountId
|
||
* @param poNumber
|
||
*/
|
||
async cancelOrder(
|
||
context: Context,
|
||
accountId: number,
|
||
poNumber: string,
|
||
): Promise<{ canceledOrderId: number }> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const orderRepo = entityManager.getRepository(LicenseOrder);
|
||
|
||
// キャンセル対象の注文を取得
|
||
const targetOrder = await orderRepo.findOne({
|
||
where: {
|
||
from_account_id: accountId,
|
||
po_number: poNumber,
|
||
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
lock: { mode: 'pessimistic_write' },
|
||
});
|
||
|
||
// キャンセル対象の注文が存在しない場合エラー
|
||
if (!targetOrder) {
|
||
throw new CancelOrderFailedError(
|
||
`Cancel order is failed. accountId: ${accountId}, poNumber: ${poNumber}`,
|
||
);
|
||
}
|
||
|
||
// 注文キャンセル処理
|
||
targetOrder.status = LICENSE_ISSUE_STATUS.CANCELED;
|
||
targetOrder.canceled_at = new Date();
|
||
await updateEntity(
|
||
orderRepo,
|
||
{ id: targetOrder.id },
|
||
targetOrder,
|
||
this.isCommentOut,
|
||
context,
|
||
);
|
||
|
||
return { canceledOrderId: targetOrder.id };
|
||
});
|
||
}
|
||
|
||
/**
|
||
* ライセンスの割当状態を取得します
|
||
* @param userId ユーザーID
|
||
* @error { Error } DBアクセス失敗時の例外
|
||
* @returns Promise<{ state: 'allocated' | 'unallocated' | 'expired' }>
|
||
*/
|
||
async getLicenseState(
|
||
context: Context,
|
||
userId: number,
|
||
): Promise<{
|
||
state:
|
||
| typeof USER_LICENSE_STATUS.ALLOCATED
|
||
| typeof USER_LICENSE_STATUS.UNALLOCATED
|
||
| typeof USER_LICENSE_STATUS.EXPIRED;
|
||
}> {
|
||
const allocatedLicense = await this.dataSource
|
||
.getRepository(License)
|
||
.findOne({
|
||
where: {
|
||
allocated_user_id: userId,
|
||
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||
},
|
||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||
});
|
||
|
||
// ライセンスが割り当てられていない場合は未割当状態
|
||
if (allocatedLicense == null) {
|
||
return { state: USER_LICENSE_STATUS.UNALLOCATED };
|
||
}
|
||
|
||
// ライセンスの有効期限が過ぎている場合は期限切れ状態
|
||
const currentDate = new DateWithZeroTime();
|
||
if (
|
||
allocatedLicense.expiry_date &&
|
||
allocatedLicense.expiry_date < currentDate
|
||
) {
|
||
return { state: USER_LICENSE_STATUS.EXPIRED };
|
||
}
|
||
|
||
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 };
|
||
});
|
||
}
|
||
}
|