OMDSCloud/dictation_server/src/repositories/licenses/licenses.repository.service.ts
makabe.t b8b3416795 Merged PR 625: セレクトのクエリに追跡用のIDと実行日時の情報を追加する
## 概要
[Task3288: セレクトのクエリに追跡用のIDと実行日時の情報を追加する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3288)

- リポジトリ内でのDB操作でSelect文となる部分にコメント(追跡ID_日時)を追加しました。
  - `find`, `fineOne`, `count`を対象にしています。
- コメントを追加するにあたってContextをリポジトリメソッドの引数に追加しています。

## レビューポイント
- 対応箇所の漏れはないでしょうか?
- コメントのつけ方は適切でしょうか?

## UIの変更
- なし

## 動作確認状況
- ローカルで確認
2023-12-13 00:00:15 +00:00

714 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Injectable, Logger } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import {
LicenseOrder,
License,
CardLicenseIssue,
CardLicense,
LicenseAllocationHistory,
} from './entity/license.entity';
import {
CARD_LICENSE_LENGTH,
LICENSE_ALLOCATED_STATUS,
LICENSE_ISSUE_STATUS,
LICENSE_TYPE,
SWITCH_FROM_TYPE,
TIERS,
} 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 { Context } from '../../common/log';
@Injectable()
export class LicensesRepositoryService {
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()}`,
});
// 重複があった場合はエラーを返却する
if (isPoNumberDuplicated) {
throw new PoNumberAlreadyExistError(`This PoNumber already used.`);
}
const repo = entityManager.getRepository(LicenseOrder);
const newLicenseOrder = repo.create(licenseOrder);
const persisted = await repo.save(newLicenseOrder);
return persisted;
},
);
return createdEntity;
}
/**
* カードライセンスを発行する
* @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 licensesRepo
.createQueryBuilder()
.insert()
.into(License)
.values(licenses)
.execute();
// カードライセンス発行テーブルを作成する
const cardLicenseIssue = new CardLicenseIssue();
cardLicenseIssue.issued_at = new Date();
const newCardLicenseIssue = cardLicenseIssueRepo.create(cardLicenseIssue);
const savedCardLicensesIssue = await cardLicenseIssueRepo.save(
newCardLicenseIssue,
);
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()}`,
});
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.generatedMaps[i].id; // Licenseテーブルの自動採番されたIDを挿入
cardLicense.issue_id = savedCardLicensesIssue.id; // CardLicenseIssueテーブルの自動採番されたIDを挿入
cardLicense.card_license_key = licenseKeys[i];
cardLicenses.push(cardLicense);
}
await cardLicenseRepo
.createQueryBuilder()
.insert()
.into(CardLicense)
.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()}`,
});
// カードライセンスが存在しなければエラー
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 licensesRepo.save(targetLicense);
// カードライセンステーブルを更新する
targetCardLicense.activated_at = new Date();
await cardLicenseRepo.save(targetCardLicense);
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<void> {
const nowDate = new Date();
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()}`,
});
// 注文が存在しない場合、エラー
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 licenseOrderRepo.update(
{ id: issuingOrder.id },
{
issued_at: nowDate,
status: LICENSE_ISSUE_STATUS.ISSUED,
},
);
// ライセンステーブルを登録(注文元)
await licenseRepo
.createQueryBuilder()
.insert()
.into(License)
.values(newLicenses)
.execute();
// 第一階層の場合はストックライセンスの概念が存在しないため、ストックライセンス変更処理は行わない
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 licenseRepo.save(licensesToUpdate);
}
});
}
/**
* 対象のアカウントの割り当て可能なライセンスを取得する
* @context Context
* @param accountId
* @param tier
* @return AllocatableLicenseInfo[]
*/
async getAllocatableLicenses(
context: Context,
myAccountId: number,
): Promise<AllocatableLicenseInfo[]> {
const nowDate = new DateWithZeroTime();
const licenseRepo = this.dataSource.getRepository(License);
// EntityManagerではorderBy句で、expiry_dateに対して複数条件でソートを使用するため出来ない為、createQueryBuilderを使用する。
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 >= :nowDate OR license.expiry_date IS NULL)',
{ nowDate },
)
.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 licenseRepo = entityManager.getRepository(License);
const licenseAllocationHistoryRepo = entityManager.getRepository(
LicenseAllocationHistory,
);
// 割り当て対象のライセンス情報を取得
const targetLicense = await licenseRepo.findOne({
where: {
id: newLicenseId,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
// ライセンスが存在しない場合はエラー
if (!targetLicense) {
throw new LicenseNotExistError(
`License not exist. licenseId: ${newLicenseId}`,
);
}
// 期限切れの場合はエラー
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()}`,
});
// 既にライセンスが割り当てられているなら、割り当てを解除
if (allocatedLicense) {
allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE;
allocatedLicense.allocated_user_id = null;
await licenseRepo.save(allocatedLicense);
// ライセンス割り当て履歴テーブルへ登録
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 licenseAllocationHistoryRepo.save(deallocationHistory);
}
// ライセンス割り当てを実施
targetLicense.status = LICENSE_ALLOCATED_STATUS.ALLOCATED;
targetLicense.allocated_user_id = userId;
// 有効期限が未設定なら365日後に設定
if (!targetLicense.expiry_date) {
targetLicense.expiry_date = new NewAllocatedLicenseExpirationDate();
}
await licenseRepo.save(targetLicense);
// 直近割り当てたライセンス種別を取得
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 licenseAllocationHistoryRepo.save(allocationHistory);
});
}
/**
* ユーザーに割り当てられているライセンスを解除する
* @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()}`,
});
// ライセンスが割り当てられていない場合はエラー
if (!allocatedLicense) {
throw new LicenseAlreadyDeallocatedError(
`License is already deallocated. userId: ${userId}`,
);
}
// ライセンスの割り当てを解除
allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE;
allocatedLicense.allocated_user_id = null;
await licenseRepo.save(allocatedLicense);
// ライセンス割り当て履歴テーブルへ登録
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 licenseAllocationHistoryRepo.save(deallocationHistory);
});
}
/**
* ライセンス注文をキャンセルする
* @param accountId
* @param poNumber
*/
async cancelOrder(
context: Context,
accountId: number,
poNumber: string,
): Promise<void> {
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()}`,
});
// キャンセル対象の注文が存在しない場合エラー
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 orderRepo.save(targetOrder);
});
}
/**
* ライセンスの割当状態を取得します
* @param userId ユーザーID
* @error { Error } DBアクセス失敗時の例外
* @returns Promise<{ state: 'allocated' | 'inallocated' | 'expired' }>
*/
async getLicenseState(
context: Context,
userId: number,
): Promise<{ state: 'allocated' | 'inallocated' | '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: 'inallocated' };
}
// ライセンスの有効期限が過ぎている場合は期限切れ状態
const currentDate = new DateWithZeroTime();
if (
allocatedLicense.expiry_date &&
allocatedLicense.expiry_date < currentDate
) {
return { state: 'expired' };
}
return { state: 'allocated' };
}
}