## 概要 [Task2934: エラーログが意図した通りに出ていないところがありそうな問題を解消する](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2934) - 何をどう変更したか、追加したライブラリなど error=Errorのログ表示をerror=××Error(エラーの原因)となるように実装 ## レビューポイント - 特にレビューしてほしい箇所 特になし ## 動作確認状況 - ユニットテスト ## 相談(必須レビュアーの方に) 今回の実装のみではまだ解消できていない箇所がいくつかみられました。 以下にその解消できなかったログをいくつかピックアップします。 ERROR [TasksService] error=Error: There is no AuthorId for the API executor. ERROR [FilesService] error=Error: blob failed ERROR [FilesService] error=Error: container not found. ERROR [UsersService] error=Error: ADB2C error ERROR [UsersService] error=Error: user not found ERROR [UsersService] error=Error: sort criteria not found ERROR [UsersService] error=Error: sort criteria not found ERROR [UsersService] error=Error: The value stored in the DB is invalid. 原因としては、throw new ××Errorではなく、throw new Errorで実装されていました。 interface Error { name: string; message: string; stack?: string; } interface ErrorConstructor { new(message?: string): Error; (message?: string): Error; readonly prototype: Error; } 上記の実装により返却されるメッセージがErrorになっているため解消されていないと考えています。 以上の事象について対応するかしないかということと、対応する場合は、どのように対応していくかをご意見いただきたいです。 本タスクで対応というよりも別タスクとして対応とMISOは想定しています。
1121 lines
37 KiB
TypeScript
1121 lines
37 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
||
import {
|
||
Between,
|
||
DataSource,
|
||
In,
|
||
IsNull,
|
||
MoreThan,
|
||
MoreThanOrEqual,
|
||
Not,
|
||
UpdateResult,
|
||
EntityManager,
|
||
} from 'typeorm';
|
||
import { User, UserArchive } from '../users/entity/user.entity';
|
||
import { Account } from './entity/account.entity';
|
||
import {
|
||
CardLicense,
|
||
License,
|
||
LicenseAllocationHistory,
|
||
LicenseAllocationHistoryArchive,
|
||
LicenseArchive,
|
||
LicenseOrder,
|
||
} from '../licenses/entity/license.entity';
|
||
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
|
||
import {
|
||
getDirection,
|
||
getTaskListSortableAttribute,
|
||
} from '../../common/types/sort/util';
|
||
import {
|
||
LICENSE_ALLOCATED_STATUS,
|
||
LICENSE_EXPIRATION_THRESHOLD_DAYS,
|
||
LICENSE_ISSUE_STATUS,
|
||
TIERS,
|
||
} from '../../constants';
|
||
import {
|
||
LicenseSummaryInfo,
|
||
PartnerLicenseInfoForRepository,
|
||
PartnerInfoFromDb,
|
||
} from '../../features/accounts/types/types';
|
||
import {
|
||
AccountNotFoundError,
|
||
AdminUserNotFoundError,
|
||
DealerAccountNotFoundError,
|
||
} from './errors/types';
|
||
import {
|
||
AlreadyLicenseAllocatedError,
|
||
AlreadyLicenseStatusChangedError,
|
||
CancellationPeriodExpiredError,
|
||
} from '../licenses/errors/types';
|
||
import { DateWithZeroTime } from '../../features/licenses/types/types';
|
||
import { Worktype } from '../worktypes/entity/worktype.entity';
|
||
import { WorktypeIdNotFoundError } from '../worktypes/errors/types';
|
||
import { OptionItem } from '../worktypes/entity/option_item.entity';
|
||
import { Task } from '../tasks/entity/task.entity';
|
||
import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity';
|
||
import { AudioFile } from '../audio_files/entity/audio_file.entity';
|
||
import { AudioOptionItem } from '../audio_option_items/entity/audio_option_item.entity';
|
||
import { UserGroup } from '../user_groups/entity/user_group.entity';
|
||
import { UserGroupMember } from '../user_groups/entity/user_group_member.entity';
|
||
import { TemplateFile } from '../template_files/entity/template_file.entity';
|
||
|
||
@Injectable()
|
||
export class AccountsRepositoryService {
|
||
constructor(private dataSource: DataSource) {}
|
||
|
||
/**
|
||
* 管理ユーザー無しでアカウントを作成する
|
||
* @param companyName
|
||
* @param country
|
||
* @param dealerAccountId
|
||
* @param tier
|
||
* @returns create
|
||
*/
|
||
async create(
|
||
companyName: string,
|
||
country: string,
|
||
dealerAccountId: number | null,
|
||
tier: number,
|
||
): Promise<Account> {
|
||
const account = new Account();
|
||
{
|
||
account.parent_account_id = dealerAccountId;
|
||
account.company_name = companyName;
|
||
account.country = country;
|
||
account.tier = tier;
|
||
}
|
||
|
||
const createdEntity = await this.dataSource.transaction(
|
||
async (entityManager) => {
|
||
const repo = entityManager.getRepository(Account);
|
||
const newAccount = repo.create(account);
|
||
const persisted = await repo.save(newAccount);
|
||
return persisted;
|
||
},
|
||
);
|
||
return createdEntity;
|
||
}
|
||
|
||
/**
|
||
* 特定の情報でアカウントを更新する
|
||
* @param account
|
||
* @returns update
|
||
*/
|
||
async update(account: Account): Promise<UpdateResult> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const repo = entityManager.getRepository(Account);
|
||
return await repo.update({ id: account.id }, account);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* プライマリ管理者とアカウント、ソート条件を同時に作成する
|
||
* @param companyName
|
||
* @param country
|
||
* @param dealerAccountId
|
||
* @param tier
|
||
* @param adminExternalUserId
|
||
* @param adminUserRole
|
||
* @param adminUserAcceptedEulaVersion
|
||
* @param adminUserAcceptedDpaVersion
|
||
* @returns account/admin user
|
||
*/
|
||
async createAccount(
|
||
companyName: string,
|
||
country: string,
|
||
dealerAccountId: number | undefined,
|
||
tier: number,
|
||
adminExternalUserId: string,
|
||
adminUserRole: string,
|
||
adminUserAcceptedEulaVersion?: string,
|
||
adminUserAcceptedDpaVersion?: string,
|
||
): Promise<{ newAccount: Account; adminUser: User }> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const account = new Account();
|
||
{
|
||
account.parent_account_id = dealerAccountId ?? null;
|
||
account.company_name = companyName;
|
||
account.country = country;
|
||
account.tier = tier;
|
||
}
|
||
const accountsRepo = entityManager.getRepository(Account);
|
||
const newAccount = accountsRepo.create(account);
|
||
const persistedAccount = await accountsRepo.save(newAccount);
|
||
|
||
// 作成されたAccountのIDを使用してユーザーを作成
|
||
const user = new User();
|
||
{
|
||
user.account_id = persistedAccount.id;
|
||
user.external_id = adminExternalUserId;
|
||
user.role = adminUserRole;
|
||
user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null;
|
||
user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null;
|
||
}
|
||
const usersRepo = entityManager.getRepository(User);
|
||
const newUser = usersRepo.create(user);
|
||
const persistedUser = await usersRepo.save(newUser);
|
||
|
||
// アカウントに管理者を設定して更新
|
||
persistedAccount.primary_admin_user_id = persistedUser.id;
|
||
|
||
const result = await accountsRepo.update(
|
||
{ id: persistedAccount.id },
|
||
persistedAccount,
|
||
);
|
||
|
||
// 想定外の更新が行われた場合はロールバックを行った上でエラー送出
|
||
if (result.affected !== 1) {
|
||
throw new Error(`invalid update. result.affected=${result.affected}`);
|
||
}
|
||
|
||
// ユーザーのタスクソート条件を作成
|
||
const sortCriteria = new SortCriteria();
|
||
{
|
||
sortCriteria.parameter = getTaskListSortableAttribute('JOB_NUMBER');
|
||
sortCriteria.direction = getDirection('ASC');
|
||
sortCriteria.user_id = persistedUser.id;
|
||
}
|
||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||
const newSortCriteria = sortCriteriaRepo.create(sortCriteria);
|
||
await sortCriteriaRepo.save(newSortCriteria);
|
||
|
||
return { newAccount: persistedAccount, adminUser: persistedUser };
|
||
});
|
||
}
|
||
|
||
/**
|
||
* プライマリ管理者とアカウント、ソート条件を同時に削除する
|
||
* @param accountId
|
||
* @returns delete
|
||
*/
|
||
async deleteAccount(accountId: number, userId: number): Promise<void> {
|
||
await this.dataSource.transaction(async (entityManager) => {
|
||
const accountsRepo = entityManager.getRepository(Account);
|
||
const usersRepo = entityManager.getRepository(User);
|
||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||
// ソート条件を削除
|
||
await sortCriteriaRepo.delete({
|
||
user_id: userId,
|
||
});
|
||
// プライマリ管理者を削除
|
||
await usersRepo.delete({ id: userId });
|
||
// アカウントを削除
|
||
await accountsRepo.delete({ id: accountId });
|
||
});
|
||
}
|
||
|
||
/**
|
||
* アカウントIDからアカウント情報を取得する
|
||
* @param id
|
||
* @returns account
|
||
*/
|
||
async findAccountById(id: number): Promise<Account> {
|
||
const account = await this.dataSource.getRepository(Account).findOne({
|
||
where: {
|
||
id: id,
|
||
},
|
||
});
|
||
|
||
if (!account) {
|
||
throw new AccountNotFoundError(`Account is Not Found.`);
|
||
}
|
||
return account;
|
||
}
|
||
|
||
/**
|
||
* ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
|
||
* 有効期限が現在日付からしきい値以内のライセンス数を取得する
|
||
* @param entityManager
|
||
* @param id
|
||
* @param currentDate
|
||
* @param expiringSoonDate
|
||
* @returns expiringSoonLicense
|
||
*/
|
||
private async getExpiringSoonLicense(
|
||
entityManager: EntityManager,
|
||
id: number,
|
||
currentDate: Date,
|
||
expiringSoonDate: Date,
|
||
): Promise<number> {
|
||
const license = entityManager.getRepository(License);
|
||
|
||
// 有効期限が現在日付からしきい値以内のライセンス数を取得する
|
||
const expiringSoonLicense = await license.count({
|
||
where: {
|
||
account_id: id,
|
||
expiry_date: Between(currentDate, expiringSoonDate),
|
||
status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
|
||
},
|
||
});
|
||
|
||
return expiringSoonLicense;
|
||
}
|
||
|
||
/**
|
||
* ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
|
||
* 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
|
||
* @param entityManager
|
||
* @param id
|
||
* @param expiringSoonDate
|
||
* @returns allocatableLicenseWithMargin
|
||
*/
|
||
private async getAllocatableLicenseWithMargin(
|
||
entityManager: EntityManager,
|
||
id: number,
|
||
expiringSoonDate: Date,
|
||
): Promise<number> {
|
||
const license = entityManager.getRepository(License);
|
||
|
||
// 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
|
||
const allocatableLicenseWithMargin = await license.count({
|
||
where: [
|
||
{
|
||
account_id: id,
|
||
status: In([
|
||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||
LICENSE_ALLOCATED_STATUS.REUSABLE,
|
||
]),
|
||
expiry_date: MoreThan(expiringSoonDate),
|
||
},
|
||
{
|
||
account_id: id,
|
||
status: In([
|
||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||
LICENSE_ALLOCATED_STATUS.REUSABLE,
|
||
]),
|
||
expiry_date: IsNull(),
|
||
},
|
||
],
|
||
});
|
||
|
||
return allocatableLicenseWithMargin;
|
||
}
|
||
|
||
/**
|
||
* アカウントIDからライセンス情報を取得する
|
||
* @param id
|
||
* @param currentDate
|
||
* @param expiringSoonDate
|
||
* @returns licenseSummary
|
||
*/
|
||
async getLicenseSummaryInfo(
|
||
id: number,
|
||
currentDate: Date,
|
||
expiringSoonDate: Date,
|
||
): Promise<{
|
||
licenseSummary: LicenseSummaryInfo;
|
||
isStorageAvailable: boolean;
|
||
}> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const license = entityManager.getRepository(License);
|
||
const licenseOrder = entityManager.getRepository(LicenseOrder);
|
||
|
||
// 有効な総ライセンス数を取得する
|
||
const totalLicense = await license.count({
|
||
where: [
|
||
{
|
||
account_id: id,
|
||
expiry_date: MoreThanOrEqual(currentDate),
|
||
status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
|
||
},
|
||
{
|
||
account_id: id,
|
||
expiry_date: IsNull(),
|
||
status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
|
||
},
|
||
],
|
||
});
|
||
|
||
// 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する
|
||
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,
|
||
},
|
||
],
|
||
});
|
||
|
||
// 総ライセンス数のうち、ユーザーに割り当てたことがあるが、現在は割り当て解除され誰にも割り当たっていないライセンス数を取得する
|
||
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,
|
||
},
|
||
],
|
||
});
|
||
|
||
// 総ライセンス数のうち、一度もユーザーに割り当てたことのないライセンス数を取得する
|
||
const freeLicense = await license.count({
|
||
where: [
|
||
{
|
||
account_id: id,
|
||
expiry_date: MoreThanOrEqual(currentDate),
|
||
status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||
},
|
||
{
|
||
account_id: id,
|
||
expiry_date: IsNull(),
|
||
status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||
},
|
||
],
|
||
});
|
||
|
||
// 有効期限が現在日付からしきい値以内のライセンス数を取得する
|
||
const expiringSoonLicense = await this.getExpiringSoonLicense(
|
||
entityManager,
|
||
id,
|
||
currentDate,
|
||
expiringSoonDate,
|
||
);
|
||
|
||
// 未発行状態あるいは発行キャンセルされた注文数を取得する
|
||
const numberOfRequesting = await licenseOrder.count({
|
||
where: {
|
||
from_account_id: id,
|
||
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
|
||
},
|
||
});
|
||
|
||
// 未発行状態あるいは発行キャンセルされた注文の総ライセンス数を取得する
|
||
const result = await licenseOrder
|
||
.createQueryBuilder('license_orders')
|
||
.select('SUM(license_orders.quantity)', 'sum')
|
||
.where('license_orders.from_account_id = :id', { id })
|
||
.andWhere('license_orders.status = :status', {
|
||
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
|
||
})
|
||
.getRawOne();
|
||
const issueRequesting = parseInt(result.sum, 10) || 0;
|
||
|
||
// 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
|
||
const allocatableLicenseWithMargin =
|
||
await this.getAllocatableLicenseWithMargin(
|
||
entityManager,
|
||
id,
|
||
expiringSoonDate,
|
||
);
|
||
|
||
// アカウントのロック状態を取得する
|
||
const isStorageAvailable = (await this.findAccountById(id)).locked;
|
||
|
||
let licenseSummary = new LicenseSummaryInfo();
|
||
licenseSummary = {
|
||
totalLicense: totalLicense,
|
||
allocatedLicense: allocatedLicense,
|
||
reusableLicense: reusableLicense,
|
||
freeLicense: freeLicense,
|
||
expiringSoonLicense: expiringSoonLicense,
|
||
allocatableLicenseWithMargin: allocatableLicenseWithMargin,
|
||
issueRequesting: issueRequesting,
|
||
numberOfRequesting: numberOfRequesting,
|
||
};
|
||
return { licenseSummary: licenseSummary, isStorageAvailable };
|
||
});
|
||
}
|
||
|
||
/**
|
||
* ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
|
||
* アカウントIDをもとに、ライセンスの注文状況を取得する
|
||
* @param id
|
||
* @param currentDate
|
||
* @param entityManager
|
||
* @returns stockLicense
|
||
* @returns issuedRequested
|
||
* @returns issueRequesting
|
||
*/
|
||
private async getAccountLicenseOrderStatus(
|
||
id: number,
|
||
currentDate: Date,
|
||
entityManager: EntityManager,
|
||
): Promise<{
|
||
stockLicense: number;
|
||
issuedRequested: number;
|
||
issueRequesting: number;
|
||
}> {
|
||
const license = entityManager.getRepository(License);
|
||
const licenseOrder = entityManager.getRepository(LicenseOrder);
|
||
|
||
// 有効な総ライセンス数を取得する
|
||
const stockLicense = await license.count({
|
||
where: [
|
||
{
|
||
account_id: id,
|
||
expiry_date: MoreThanOrEqual(currentDate),
|
||
status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
|
||
},
|
||
{
|
||
account_id: id,
|
||
expiry_date: IsNull(),
|
||
status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
|
||
},
|
||
],
|
||
});
|
||
|
||
// 子アカウントからの、未発行状態あるいは発行キャンセルされた注文の総ライセンス数を取得する
|
||
const issuedRequestedSqlResult = await licenseOrder
|
||
.createQueryBuilder('license_orders')
|
||
.select('SUM(license_orders.quantity)', 'sum')
|
||
.where('license_orders.to_account_id = :id', { id })
|
||
.andWhere('license_orders.status = :status', {
|
||
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
|
||
})
|
||
.getRawOne();
|
||
const issuedRequested = parseInt(issuedRequestedSqlResult.sum, 10) || 0;
|
||
|
||
// 未発行状態あるいは発行キャンセルされた注文の総ライセンス数を取得する
|
||
const issuedRequestingSqlResult = await licenseOrder
|
||
.createQueryBuilder('license_orders')
|
||
.select('SUM(license_orders.quantity)', 'sum')
|
||
.where('license_orders.from_account_id = :id', { id })
|
||
.andWhere('license_orders.status = :status', {
|
||
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
|
||
})
|
||
.getRawOne();
|
||
const issuedRequesting = parseInt(issuedRequestingSqlResult.sum, 10) || 0;
|
||
|
||
return {
|
||
stockLicense: stockLicense,
|
||
issuedRequested: issuedRequested,
|
||
issueRequesting: issuedRequesting,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* アカウントIDをもとに、自分と子アカウントのライセンス情報を取得する
|
||
* @param id
|
||
* @param offset
|
||
* @param limit
|
||
* @returns total: 総件数
|
||
* @returns ownPartnerLicenseFromRepository: リポジトリから取得した自アカウントのライセンス情報
|
||
* @returns childrenPartnerLicensesFromRepository: リポジトリから取得した子アカウントのライセンス情報
|
||
*/
|
||
async getPartnerLicense(
|
||
id: number,
|
||
currentDate: Date,
|
||
expiringSoonDate: Date,
|
||
offset: number,
|
||
limit: number,
|
||
): Promise<{
|
||
total: number;
|
||
ownPartnerLicenseFromRepository: PartnerLicenseInfoForRepository;
|
||
childPartnerLicensesFromRepository: PartnerLicenseInfoForRepository[];
|
||
}> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const account = entityManager.getRepository(Account);
|
||
|
||
// 自アカウントの情報を取得する
|
||
const ownAccount = await account.findOne({
|
||
where: {
|
||
id: id,
|
||
},
|
||
});
|
||
if (!ownAccount) {
|
||
throw new AccountNotFoundError(`Account is Not Found.`);
|
||
}
|
||
|
||
// 自アカウントのライセンス注文状況を取得する
|
||
const ownLicenseOrderStatus = await this.getAccountLicenseOrderStatus(
|
||
id,
|
||
currentDate,
|
||
entityManager,
|
||
);
|
||
|
||
// 自アカウントの戻り値を設定する
|
||
const ownPartnerLicenseFromRepository: PartnerLicenseInfoForRepository = {
|
||
accountId: id,
|
||
tier: ownAccount.tier,
|
||
companyName: ownAccount.company_name,
|
||
stockLicense: ownLicenseOrderStatus.stockLicense,
|
||
issuedRequested: ownLicenseOrderStatus.issuedRequested,
|
||
issueRequesting: ownLicenseOrderStatus.issueRequesting,
|
||
};
|
||
|
||
// 子アカウントのアカウント情報を取得する
|
||
const childAccounts = await account.find({
|
||
where: {
|
||
parent_account_id: id,
|
||
},
|
||
order: {
|
||
company_name: 'ASC',
|
||
},
|
||
take: limit,
|
||
skip: offset,
|
||
});
|
||
|
||
// 各子アカウントのライセンス注文状況を取得する
|
||
const childPartnerLicensesFromRepository: PartnerLicenseInfoForRepository[] =
|
||
[];
|
||
for (const childAccount of childAccounts) {
|
||
// ライセンス注文状況を取得する
|
||
const childLicenseOrderStatus = await this.getAccountLicenseOrderStatus(
|
||
childAccount.id,
|
||
currentDate,
|
||
entityManager,
|
||
);
|
||
|
||
// 第五の不足数を算出するためのライセンス数情報を取得する
|
||
let expiringSoonLicense: number = 0;
|
||
let allocatableLicenseWithMargin: number = 0;
|
||
if (childAccount.tier === TIERS.TIER5) {
|
||
expiringSoonLicense = await this.getExpiringSoonLicense(
|
||
entityManager,
|
||
childAccount.id,
|
||
currentDate,
|
||
expiringSoonDate,
|
||
);
|
||
allocatableLicenseWithMargin =
|
||
await this.getAllocatableLicenseWithMargin(
|
||
entityManager,
|
||
childAccount.id,
|
||
expiringSoonDate,
|
||
);
|
||
}
|
||
|
||
// 戻り値用の値を設定
|
||
const childPartnerLicenseFromRepository: PartnerLicenseInfoForRepository =
|
||
{
|
||
accountId: childAccount.id,
|
||
tier: childAccount.tier,
|
||
companyName: childAccount.company_name,
|
||
stockLicense: childLicenseOrderStatus.stockLicense,
|
||
issuedRequested: childLicenseOrderStatus.issuedRequested,
|
||
issueRequesting: childLicenseOrderStatus.issueRequesting,
|
||
expiringSoonLicense: expiringSoonLicense,
|
||
allocatableLicenseWithMargin: allocatableLicenseWithMargin,
|
||
};
|
||
|
||
childPartnerLicensesFromRepository.push(
|
||
childPartnerLicenseFromRepository,
|
||
);
|
||
}
|
||
|
||
// limit/offsetによらない総件数を取得する
|
||
const total = await account.count({
|
||
where: {
|
||
parent_account_id: id,
|
||
},
|
||
});
|
||
|
||
return {
|
||
total: total,
|
||
ownPartnerLicenseFromRepository: ownPartnerLicenseFromRepository,
|
||
childPartnerLicensesFromRepository: childPartnerLicensesFromRepository,
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Dealer(Tier4)アカウント情報を取得する
|
||
* @returns dealer accounts
|
||
*/
|
||
async findDealerAccounts(): Promise<Account[]> {
|
||
const accounts = await this.dataSource.getRepository(Account).find({
|
||
where: {
|
||
tier: TIERS.TIER4,
|
||
},
|
||
});
|
||
|
||
return accounts;
|
||
}
|
||
|
||
/**
|
||
* 対象のアカウントIDの親世代のアカウントIDをすべて取得する
|
||
* 順番は、階層(tier)の下位から上位に向かって格納
|
||
* @param targetAccountId
|
||
* @returns accountIds
|
||
*/
|
||
async getHierarchyParents(targetAccountId: number): Promise<number[]> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const accountRepository = entityManager.getRepository(Account);
|
||
const maxTierDifference = TIERS.TIER5 - TIERS.TIER1;
|
||
const parentAccountIds: number[] = [];
|
||
|
||
let currentAccountId = targetAccountId;
|
||
// システム的な最大の階層差異分、親を参照する
|
||
for (let i = 0; i < maxTierDifference; i++) {
|
||
const account = await accountRepository.findOne({
|
||
where: {
|
||
id: currentAccountId,
|
||
},
|
||
});
|
||
if (!account) {
|
||
break;
|
||
}
|
||
if (!account.parent_account_id) {
|
||
throw new Error("Parent account doesn't exist.");
|
||
}
|
||
|
||
parentAccountIds.push(account.parent_account_id);
|
||
currentAccountId = account.parent_account_id;
|
||
}
|
||
|
||
return parentAccountIds;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 注文元アカウントIDとPOナンバーに紐づくライセンス発行をキャンセルする
|
||
* @param orderedAccountId:キャンセルしたい発行の注文元アカウントID
|
||
* @param poNumber:POナンバー
|
||
*/
|
||
async cancelIssue(orderedAccountId: 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: orderedAccountId,
|
||
po_number: poNumber,
|
||
status: LICENSE_ISSUE_STATUS.ISSUED,
|
||
},
|
||
});
|
||
|
||
// キャンセル対象の発行が存在しない場合エラー
|
||
if (!targetOrder) {
|
||
throw new AlreadyLicenseStatusChangedError(
|
||
`Cancel issue is failed. Already license order status changed. fromAccountId: ${orderedAccountId}, poNumber: ${poNumber}`,
|
||
);
|
||
}
|
||
|
||
// キャンセル可能な日付(発行日から14日経過)かどうかに判定する時刻を取得する
|
||
const currentDateWithoutTime = new DateWithZeroTime();
|
||
const issuedDateWithoutTime = new DateWithZeroTime(targetOrder.issued_at);
|
||
const timeDifference =
|
||
currentDateWithoutTime.getTime() - issuedDateWithoutTime.getTime();
|
||
const daysDifference = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||
// 発行日から14日経過しているかをチェック
|
||
if (daysDifference > LICENSE_EXPIRATION_THRESHOLD_DAYS) {
|
||
throw new CancellationPeriodExpiredError(
|
||
`Cancel issue is failed. Cancellation period expired. fromAccountId: ${orderedAccountId}, poNumber: ${poNumber}`,
|
||
);
|
||
}
|
||
// すでに割り当て済みライセンスを含む注文か確認する
|
||
const licenseRepo = entityManager.getRepository(License);
|
||
const allocatedLicense = await licenseRepo.findOne({
|
||
where: {
|
||
order_id: targetOrder.id,
|
||
status: Not(LICENSE_ALLOCATED_STATUS.UNALLOCATED),
|
||
},
|
||
});
|
||
|
||
// 存在した場合エラー
|
||
if (allocatedLicense) {
|
||
throw new AlreadyLicenseAllocatedError(
|
||
`Cancel issue is failed. Already license allocated. fromAccountId: ${orderedAccountId}, poNumber: ${poNumber}`,
|
||
);
|
||
}
|
||
|
||
// 更新用の変数に値をコピー
|
||
const updatedOrder = { ...targetOrder };
|
||
|
||
// 注文を発行待ちに戻す
|
||
updatedOrder.issued_at = null;
|
||
updatedOrder.status = LICENSE_ISSUE_STATUS.ISSUE_REQUESTING;
|
||
await orderRepo.save(updatedOrder);
|
||
// 発行時に削除したライセンスを未割当に戻す
|
||
await licenseRepo.update(
|
||
{ delete_order_id: targetOrder.id },
|
||
{
|
||
status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||
deleted_at: null,
|
||
delete_order_id: null,
|
||
},
|
||
);
|
||
// 発行時に発行されたライセンスを削除する
|
||
await licenseRepo.delete({ order_id: targetOrder.id });
|
||
});
|
||
}
|
||
|
||
/**
|
||
* アカウントIDをもとに、パートナー一覧を取得する
|
||
* @param id
|
||
* @param limit
|
||
* @param offset
|
||
* @returns total: 総件数
|
||
* @returns partners: DBから取得できるパートナー一覧情報
|
||
*/
|
||
async getPartners(
|
||
id: number,
|
||
limit: number,
|
||
offset: number,
|
||
): Promise<{
|
||
total: number;
|
||
partnersInfo: PartnerInfoFromDb[];
|
||
}> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const accountRepo = entityManager.getRepository(Account);
|
||
|
||
// limit/offsetによらない総件数を取得する
|
||
const total = await accountRepo.count({
|
||
where: {
|
||
parent_account_id: id,
|
||
},
|
||
});
|
||
|
||
const partnerAccounts = await accountRepo.find({
|
||
where: {
|
||
parent_account_id: id,
|
||
},
|
||
order: {
|
||
company_name: 'ASC',
|
||
},
|
||
take: limit,
|
||
skip: offset,
|
||
});
|
||
|
||
// ADB2Cから情報を取得するための外部ユーザIDを取得する(念のためプライマリ管理者IDが存在しない場合を考慮)
|
||
const primaryUserIds = partnerAccounts.flatMap((x) => {
|
||
if (x.primary_admin_user_id) {
|
||
return [x.primary_admin_user_id];
|
||
} else if (x.secondary_admin_user_id) {
|
||
return [x.secondary_admin_user_id];
|
||
} else {
|
||
return [];
|
||
}
|
||
});
|
||
const userRepo = entityManager.getRepository(User);
|
||
const primaryUsers = await userRepo.find({
|
||
where: {
|
||
id: In(primaryUserIds),
|
||
},
|
||
});
|
||
|
||
// アカウント情報とプライマリ管理者の外部ユーザIDをマージ
|
||
const partners = partnerAccounts.map((account) => {
|
||
const primaryUser = primaryUsers.find(
|
||
(user) =>
|
||
user.id === account.primary_admin_user_id ||
|
||
user.id === account.secondary_admin_user_id,
|
||
);
|
||
if (!primaryUser) {
|
||
throw new AdminUserNotFoundError(
|
||
`Primary admin user is not found. id: ${account.primary_admin_user_id}, account_id: ${account.id}`,
|
||
);
|
||
}
|
||
|
||
return {
|
||
name: account.company_name,
|
||
tier: account.tier,
|
||
accountId: account.id,
|
||
country: account.country,
|
||
primaryAccountExternalId: primaryUser.external_id,
|
||
dealerManagement: account.delegation_permission,
|
||
};
|
||
});
|
||
|
||
return {
|
||
total: total,
|
||
partnersInfo: partners,
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 一階層上のアカウントを取得する
|
||
* @param accountId
|
||
* @param tier
|
||
* @returns account: 一階層上のアカウント
|
||
*/
|
||
async getOneUpperTierAccount(
|
||
accountId: number,
|
||
tier: number,
|
||
): Promise<Account | null> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const accountRepo = entityManager.getRepository(Account);
|
||
return await accountRepo.findOne({
|
||
where: {
|
||
id: accountId,
|
||
tier: tier - 1,
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* アカウント情報を保存する
|
||
* @param myAccountId
|
||
* @param tier
|
||
* @param delegationPermission
|
||
* @param primaryAdminUserId
|
||
* @param parentAccountId
|
||
* @param secondryAdminUserId
|
||
*/
|
||
async updateAccountInfo(
|
||
myAccountId: number,
|
||
tier: number,
|
||
delegationPermission: boolean,
|
||
primaryAdminUserId: number,
|
||
parentAccountId?: number,
|
||
secondryAdminUserId?: number,
|
||
): Promise<void> {
|
||
await this.dataSource.transaction(async (entityManager) => {
|
||
// ディーラーアカウントが指定されている場合、存在チェックを行う
|
||
if (parentAccountId) {
|
||
const dealerAccount = await this.getOneUpperTierAccount(
|
||
parentAccountId,
|
||
tier,
|
||
);
|
||
// 取得できない場合、エラー
|
||
if (!dealerAccount) {
|
||
throw new DealerAccountNotFoundError(
|
||
`Dealer account is not found. id: ${parentAccountId}}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
const userRepo = entityManager.getRepository(User);
|
||
// プライマリ管理者ユーザーの存在チェック
|
||
if (primaryAdminUserId) {
|
||
const primaryAdminUser = await userRepo.findOne({
|
||
where: {
|
||
id: primaryAdminUserId,
|
||
account_id: myAccountId,
|
||
email_verified: true,
|
||
},
|
||
});
|
||
if (!primaryAdminUser) {
|
||
throw new AdminUserNotFoundError(
|
||
`Primary admin user is not found or email not verified. id: ${primaryAdminUserId}, account_id: ${myAccountId}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// セカンダリ管理者ユーザーの存在チェック
|
||
if (secondryAdminUserId) {
|
||
const secondryAdminUser = await userRepo.findOne({
|
||
where: {
|
||
id: secondryAdminUserId,
|
||
account_id: myAccountId,
|
||
email_verified: true,
|
||
},
|
||
});
|
||
if (!secondryAdminUser) {
|
||
throw new AdminUserNotFoundError(
|
||
`Secondary admin user is not found or email not verified. id: ${secondryAdminUserId}, account_id: ${myAccountId}`,
|
||
);
|
||
}
|
||
}
|
||
const accountRepo = entityManager.getRepository(Account);
|
||
// アカウント情報を更新
|
||
await accountRepo.update(
|
||
{ id: myAccountId },
|
||
{
|
||
parent_account_id: parentAccountId ?? null,
|
||
delegation_permission: delegationPermission,
|
||
primary_admin_user_id: primaryAdminUserId,
|
||
secondary_admin_user_id: secondryAdminUserId ?? null,
|
||
},
|
||
);
|
||
});
|
||
}
|
||
|
||
/*
|
||
* ActiveWorktypeIdを更新する
|
||
* @param accountId
|
||
* @param [id] ActiveWorktypeIdの内部ID
|
||
* @returns active worktype id
|
||
*/
|
||
async updateActiveWorktypeId(
|
||
accountId: number,
|
||
id?: number | undefined,
|
||
): Promise<void> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
const worktypeRepo = entityManager.getRepository(Worktype);
|
||
const accountRepo = entityManager.getRepository(Account);
|
||
|
||
if (id) {
|
||
// 自アカウント内に指定IDのワークタイプが存在するか確認
|
||
const worktype = await worktypeRepo.findOne({
|
||
where: { account_id: accountId, id: id },
|
||
});
|
||
|
||
// ワークタイプが存在しない場合はエラー
|
||
if (!worktype) {
|
||
throw new WorktypeIdNotFoundError(`Worktype is not found. id: ${id}`);
|
||
}
|
||
}
|
||
|
||
// アカウントのActiveWorktypeIDを更新
|
||
await accountRepo.update(
|
||
{ id: accountId },
|
||
{ active_worktype_id: id ?? null },
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 指定されたアカウントを削除する
|
||
* @param accountId
|
||
* @returns users 削除対象のユーザー
|
||
*/
|
||
async deleteAccountAndInsertArchives(accountId: number): Promise<User[]> {
|
||
return await this.dataSource.transaction(async (entityManager) => {
|
||
// 削除対象のユーザーを退避テーブルに退避
|
||
const users = await this.dataSource.getRepository(User).find({
|
||
where: {
|
||
account_id: accountId,
|
||
},
|
||
});
|
||
const userArchiveRepo = entityManager.getRepository(UserArchive);
|
||
await userArchiveRepo
|
||
.createQueryBuilder()
|
||
.insert()
|
||
.into(UserArchive)
|
||
.values(users)
|
||
.execute();
|
||
|
||
// 削除対象のライセンスを退避テーブルに退避
|
||
const licenses = await this.dataSource.getRepository(License).find({
|
||
where: {
|
||
account_id: accountId,
|
||
},
|
||
});
|
||
const licenseArchiveRepo = entityManager.getRepository(LicenseArchive);
|
||
await licenseArchiveRepo
|
||
.createQueryBuilder()
|
||
.insert()
|
||
.into(LicenseArchive)
|
||
.values(licenses)
|
||
.execute();
|
||
|
||
// 削除対象のライセンス割り当て履歴を退避テーブルに退避
|
||
const licenseHistories = await this.dataSource
|
||
.getRepository(LicenseAllocationHistory)
|
||
.find({
|
||
where: {
|
||
account_id: accountId,
|
||
},
|
||
});
|
||
const licenseHistoryArchiveRepo = entityManager.getRepository(
|
||
LicenseAllocationHistoryArchive,
|
||
);
|
||
await licenseHistoryArchiveRepo
|
||
.createQueryBuilder()
|
||
.insert()
|
||
.into(LicenseAllocationHistoryArchive)
|
||
.values(licenseHistories)
|
||
.execute();
|
||
|
||
// アカウントを削除
|
||
const accountRepo = entityManager.getRepository(Account);
|
||
await accountRepo.delete({ id: accountId });
|
||
|
||
// ライセンス系(card_license_issue以外)のテーブルのレコードを削除する
|
||
const orderRepo = entityManager.getRepository(LicenseOrder);
|
||
await orderRepo.delete({
|
||
from_account_id: accountId,
|
||
});
|
||
const licenseRepo = entityManager.getRepository(License);
|
||
const targetLicenses = await licenseRepo.find({
|
||
where: {
|
||
account_id: accountId,
|
||
},
|
||
});
|
||
const cardLicenseRepo = entityManager.getRepository(CardLicense);
|
||
await cardLicenseRepo.delete({
|
||
license_id: In(targetLicenses.map((license) => license.id)),
|
||
});
|
||
await licenseRepo.delete({
|
||
account_id: accountId,
|
||
});
|
||
const LicenseAllocationHistoryRepo = entityManager.getRepository(
|
||
LicenseAllocationHistory,
|
||
);
|
||
await LicenseAllocationHistoryRepo.delete({
|
||
account_id: accountId,
|
||
});
|
||
|
||
// ワークタイプ系のテーブルのレコードを削除する
|
||
const worktypeRepo = entityManager.getRepository(Worktype);
|
||
const taggerWorktypes = await worktypeRepo.find({
|
||
where: { account_id: accountId },
|
||
});
|
||
|
||
const optionItemRepo = entityManager.getRepository(OptionItem);
|
||
await optionItemRepo.delete({
|
||
worktype_id: In(taggerWorktypes.map((worktype) => worktype.id)),
|
||
});
|
||
await worktypeRepo.delete({ account_id: accountId });
|
||
|
||
// タスク系のテーブルのレコードを削除する
|
||
const taskRepo = entityManager.getRepository(Task);
|
||
const targetTasks = await taskRepo.find({
|
||
where: {
|
||
account_id: accountId,
|
||
},
|
||
});
|
||
const checkoutPermissionRepo =
|
||
entityManager.getRepository(CheckoutPermission);
|
||
await checkoutPermissionRepo.delete({
|
||
task_id: In(targetTasks.map((task) => task.id)),
|
||
});
|
||
await taskRepo.delete({
|
||
account_id: accountId,
|
||
});
|
||
|
||
// オーディオファイル系のテーブルのレコードを削除する
|
||
const audioFileRepo = entityManager.getRepository(AudioFile);
|
||
const targetaudioFiles = await audioFileRepo.find({
|
||
where: {
|
||
account_id: accountId,
|
||
},
|
||
});
|
||
const audioOptionItemsRepo = entityManager.getRepository(AudioOptionItem);
|
||
await audioOptionItemsRepo.delete({
|
||
audio_file_id: In(targetaudioFiles.map((audioFile) => audioFile.id)),
|
||
});
|
||
await audioFileRepo.delete({
|
||
account_id: accountId,
|
||
});
|
||
|
||
// ユーザーグループ系のテーブルのレコードを削除する
|
||
const userGroupRepo = entityManager.getRepository(UserGroup);
|
||
const targetUserGroup = await userGroupRepo.find({
|
||
where: {
|
||
account_id: accountId,
|
||
},
|
||
});
|
||
const userGroupMemberRepo = entityManager.getRepository(UserGroupMember);
|
||
await userGroupMemberRepo.delete({
|
||
user_group_id: In(targetUserGroup.map((userGroup) => userGroup.id)),
|
||
});
|
||
await userGroupRepo.delete({
|
||
account_id: accountId,
|
||
});
|
||
|
||
// テンプレートファイルテーブルのレコードを削除する
|
||
const templateFileRepo = entityManager.getRepository(TemplateFile);
|
||
await templateFileRepo.delete({ account_id: accountId });
|
||
|
||
// ユーザテーブルのレコードを削除する
|
||
const userRepo = entityManager.getRepository(User);
|
||
await userRepo.delete({
|
||
account_id: accountId,
|
||
});
|
||
|
||
// ソート条件のテーブルのレコードを削除する
|
||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||
await sortCriteriaRepo.delete({
|
||
user_id: In(users.map((user) => user.id)),
|
||
});
|
||
return users;
|
||
});
|
||
}
|
||
}
|