Kentaro Fukunaga 75f0a49fc1 Merged PR 831: 親アカウント変更API実装
## 概要
[Task3853: 親アカウント変更API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3853)

- 親アカウント切り替えAPIを実装しました。

## レビューポイント
- Service層の関数の分け方に改善点ないか?
- テストケースで他にあったほうがいいものや観点などあるか?

## UIの変更
- なし

## クエリの変更
- なし

## 動作確認状況
- ローカルで全テスト通ることを確認
- 行った修正がデグレを発生させていないことを確認できるか
    - 新規APIの実装のため既存実装に変更なし
2024-03-18 05:47:24 +00:00

2837 lines
85 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 { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import {
AdB2cService,
ConflictError,
isConflictError,
} from '../../gateways/adb2c/adb2c.service';
import { Account } from '../../repositories/accounts/entity/account.entity';
import { User } from '../../repositories/users/entity/user.entity';
import {
TIERS,
USER_ROLES,
OPTION_ITEM_VALUE_TYPE,
MANUAL_RECOVERY_REQUIRED,
LICENSE_ISSUE_STATUS,
BLOB_STORAGE_REGION_AU,
BLOB_STORAGE_REGION_EU,
BLOB_STORAGE_REGION_US,
} from '../../constants';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import {
TypistGroup,
GetPartnerLicensesResponse,
PartnerLicenseInfo,
GetOrderHistoriesResponse,
LicenseOrder,
GetDealersResponse,
Dealer,
GetMyAccountResponse,
GetTypistGroupResponse,
GetWorktypesResponse,
GetOptionItemsResponse,
GetPartnersResponse,
PostWorktypeOptionItem,
Author,
Partner,
GetCompanyNameResponse,
} from './types/types';
import {
DateWithZeroTime,
ExpirationThresholdDate,
} from '../licenses/types/types';
import { GetLicenseSummaryResponse, Typist } from './types/types';
import { UserNotFoundError } from '../../repositories/users/errors/types';
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
import { makePassword } from '../../common/password';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import {
AccountNotFoundError,
AdminUserNotFoundError,
CountryMismatchError,
DealerAccountNotFoundError,
HierarchyMismatchError,
RegionMismatchError,
} from '../../repositories/accounts/errors/types';
import { Context } from '../../common/log';
import {
LicensesShortageError,
AlreadyIssuedError,
OrderNotFoundError,
AlreadyLicenseStatusChangedError,
AlreadyLicenseAllocatedError,
CancellationPeriodExpiredError,
} from '../../repositories/licenses/errors/types';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import {
AssignedWorkflowDeleteFailedError,
ExistsCheckoutPermissionDeleteFailedError,
TypistGroupNameAlreadyExistError,
TypistGroupNotExistError,
TypistIdInvalidError,
} from '../../repositories/user_groups/errors/types';
import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service';
import {
WorktypeIdAlreadyExistsError,
WorktypeIdInUseError,
WorktypeIdMaxCountError,
WorktypeIdNotFoundError,
} from '../../repositories/worktypes/errors/types';
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
@Injectable()
export class AccountsService {
constructor(
private readonly accountRepository: AccountsRepositoryService,
private readonly licensesRepository: LicensesRepositoryService,
private readonly usersRepository: UsersRepositoryService,
private readonly userGroupsRepository: UserGroupsRepositoryService,
private readonly worktypesRepository: WorktypesRepositoryService,
private readonly adB2cService: AdB2cService,
private readonly sendgridService: SendGridService,
private readonly blobStorageService: BlobstorageService,
) {}
private readonly logger = new Logger(AccountsService.name);
/**
* 第五階層用のライセンス情報を取得する
* @param accountId
* @returns LicenseSummary
*/
async getLicenseSummary(
context: Context,
accountId: number,
): Promise<GetLicenseSummaryResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getLicenseSummary.name
} | params: { ` + `accountId: ${accountId}, };`,
);
try {
const currentDate = new DateWithZeroTime();
const expiringSoonDate = new ExpirationThresholdDate(
currentDate.getTime(),
);
const { licenseSummary, isStorageAvailable } =
await this.accountRepository.getLicenseSummaryInfo(
context,
accountId,
currentDate,
expiringSoonDate,
);
const {
allocatableLicenseWithMargin,
expiringSoonLicense,
totalLicense,
allocatedLicense,
reusableLicense,
freeLicense,
issueRequesting,
numberOfRequesting,
} = licenseSummary;
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,
reusableLicense,
freeLicense,
expiringWithin14daysLicense: expiringSoonLicense,
issueRequesting,
numberOfRequesting,
storageSize: size,
usedSize: used,
shortage,
isStorageAvailable,
};
return licenseSummaryResponse;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.error(
`[${context.getTrackingId()}] get licenseSummary failed`,
);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getLicenseSummary.name}`,
);
}
}
/**
* アカウント情報をDBに作成する
* @param companyName
* @param country
* @param [dealerAccountId]
* @returns account
*/
async createAccount(
context: Context,
companyName: string,
country: string,
dealerAccountId: number | undefined,
email: string,
password: string,
username: string,
role: string,
acceptedEulaVersion: string,
acceptedPrivacyNoticeVersion: string,
acceptedDpaVersion: string,
): Promise<{ accountId: number; userId: number; externalUserId: string }> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.createAccount.name
} | params: { ` +
`dealerAccountId: ${dealerAccountId}, ` +
`role: ${role}, ` +
`acceptedEulaVersion: ${acceptedEulaVersion}, ` +
`acceptedPrivacyNoticeVersion: ${acceptedPrivacyNoticeVersion}, ` +
`acceptedDpaVersion: ${acceptedDpaVersion} };`,
);
try {
let externalUser: { sub: string } | ConflictError;
try {
// idpにユーザーを作成
externalUser = await this.adB2cService.createUser(
context,
email,
password,
username,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.error(
`[${context.getTrackingId()}] create externalUser failed`,
);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// メールアドレス重複エラー
if (isConflictError(externalUser)) {
this.logger.error(
`[${context.getTrackingId()}] email conflict. externalUser: ${externalUser}`,
);
throw new HttpException(
makeErrorResponse('E010301'),
HttpStatus.BAD_REQUEST,
);
}
let account: Account;
let user: User;
try {
// アカウントと管理者をセットで作成
const { newAccount, adminUser } =
await this.accountRepository.createAccount(
context,
companyName,
country,
dealerAccountId,
TIERS.TIER5,
externalUser.sub,
role,
acceptedEulaVersion,
acceptedPrivacyNoticeVersion,
acceptedDpaVersion,
);
account = newAccount;
user = adminUser;
this.logger.log(
`[${context.getTrackingId()}] adminUser.external_id: ${
user.external_id
}`,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.error(`[${context.getTrackingId()}] create account failed`);
//リカバリ処理
// idpのユーザーを削除
await this.deleteAdB2cUser(externalUser.sub, context);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// 新規作成アカウント用のBlobコンテナを作成
try {
await this.blobStorageService.createContainer(
context,
account.id,
country,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.error(
`[${context.getTrackingId()}] create container failed`,
);
//リカバリ処理
// idpのユーザーを削除
await this.deleteAdB2cUser(externalUser.sub, context);
// DBのアカウントを削除
await this.deleteAccount(account.id, user.id, context);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
await this.sendgridService.sendMailWithU102(
context,
email,
account.id,
user.id,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.error(`[${context.getTrackingId()}] send E-mail failed`);
//リカバリ処理
// idpのユーザーを削除
await this.deleteAdB2cUser(externalUser.sub, context);
// DBのアカウントを削除
await this.deleteAccount(account.id, user.id, context);
// Blobコンテナを削除
await this.deleteBlobContainer(account.id, country, context);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
return {
accountId: account.id,
userId: user.id,
externalUserId: user.external_id,
};
} catch (e) {
throw e;
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.createAccount.name}`,
);
}
}
// AdB2cのユーザーを削除
// TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
private async deleteAdB2cUser(
externalUserId: string,
context: Context,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.createAccount.name
} | params: { ` + `externalUserId: ${externalUserId}};`,
);
try {
await this.adB2cService.deleteUser(externalUserId, context);
this.logger.log(
`[${context.getTrackingId()}] delete externalUser: ${externalUserId} | params: { ` +
`externalUserId: ${externalUserId}, };`,
);
} catch (error) {
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
this.logger.error(
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete externalUser: ${externalUserId}`,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.deleteAdB2cUser.name}`,
);
}
}
// DBのアカウントを削除
private async deleteAccount(
accountId: number,
userId: number,
context: Context,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.deleteAccount.name
} | params: { accountId: ${accountId}, userId: ${userId} };`,
);
try {
await this.accountRepository.deleteAccount(context, accountId, userId);
this.logger.log(
`[${context.getTrackingId()}] delete account: ${accountId}, user: ${userId}`,
);
} catch (error) {
this.logger.error(`[${context.getTrackingId()}] error=${error}`);
this.logger.error(
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete account: ${accountId}, user: ${userId}`,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.deleteAccount.name}`,
);
}
}
// Blobコンテナを削除
// TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
private async deleteBlobContainer(
accountId: number,
country: string,
context: Context,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.deleteBlobContainer.name
} | params: { accountId: ${accountId} };`,
);
try {
await this.blobStorageService.deleteContainer(
context,
accountId,
country,
);
this.logger.log(
`[${context.getTrackingId()}] delete container: ${accountId}, country: ${country}`,
);
} catch (error) {
this.logger.error(
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete container: ${accountId}, country: ${country}`,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.deleteBlobContainer.name}`,
);
}
}
/**
* パラメータのユーザIDからアカウント情報を取得する
* @param externalId
* @returns GetMyAccountResponse
*/
async getAccountInfo(
context: Context,
externalId: string,
): Promise<GetMyAccountResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getAccountInfo.name
} | params: { ` + `externalId: ${externalId}, };`,
);
try {
const userInfo = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
const accountInfo = await this.accountRepository.findAccountById(
context,
userInfo.account_id,
);
let parentInfo: Account | undefined;
if (accountInfo.parent_account_id) {
parentInfo = await this.accountRepository.findAccountById(
context,
accountInfo.parent_account_id,
);
}
return {
account: {
accountId: userInfo.account_id,
companyName: accountInfo.company_name,
tier: accountInfo.tier,
country: accountInfo.country,
parentAccountId: accountInfo.parent_account_id ?? undefined,
delegationPermission: accountInfo.delegation_permission,
autoFileDelete: accountInfo.auto_file_delete,
fileRetentionDays: accountInfo.file_retention_days,
primaryAdminUserId: accountInfo.primary_admin_user_id ?? undefined,
secondryAdminUserId: accountInfo.secondary_admin_user_id ?? undefined,
parentAccountName: parentInfo ? parentInfo.company_name : undefined,
},
};
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getAccountInfo.name}`,
);
}
}
async getTypistGroups(
context: Context,
externalId: string,
): Promise<TypistGroup[]> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getTypistGroups.name
} | params: { externalId: ${externalId} };`,
);
// TypistGroup取得
try {
const user = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
const userGroups = await this.userGroupsRepository.getUserGroups(
context,
user.account_id,
);
return userGroups.map((x) => ({ id: x.id, name: x.name }));
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getTypistGroups.name}`,
);
}
}
/**
* IDを指定してタイピストグループを取得する
* @param context
* @param externalId
* @param typistGroupId
* @returns typist group
*/
async getTypistGroup(
context: Context,
externalId: string,
typistGroupId: number,
): Promise<GetTypistGroupResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getTypistGroup.name
} | params: { externalId: ${externalId}, typistGroupId: ${typistGroupId} };`,
);
try {
const { account_id } = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
const { name, userGroupMembers } =
await this.userGroupsRepository.getTypistGroup(
context,
account_id,
typistGroupId,
);
if (!userGroupMembers) {
throw new TypistGroupNotExistError(
`Typist Group is not exist. typistGroupId: ${typistGroupId}`,
);
}
return {
typistGroupName: name,
typistIds: userGroupMembers.map((x) => x.user_id),
};
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TypistGroupNotExistError:
throw new HttpException(
makeErrorResponse('E010908'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getTypistGroup.name}`,
);
}
}
/**
* Gets typists
* @param externalId
* @returns typists
*/
async getTypists(context: Context, externalId: string): Promise<Typist[]> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getTypists.name
} | params: { externalId: ${externalId} };`,
);
// Typist取得
try {
const typistUsers = await this.usersRepository.findTypistUsers(
context,
externalId,
);
const externalIds = typistUsers.map((x) => x.external_id);
// B2Cからユーザー名を取得する
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
const typists = typistUsers.map((x) => {
const user = adb2cUsers.find((adb2c) => adb2c.id === x.external_id);
if (!user) {
throw new Error(
`user not found. externalId: ${x.external_id}, userId: ${x.id}`,
);
}
return {
id: x.id,
name: user.displayName,
};
});
return typists;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getTypists.name}`,
);
}
}
/**
* アカウント内のAuthorを取得する
* @param context
* @param externalId
* @returns authors
*/
async getAuthors(context: Context, externalId: string): Promise<Author[]> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getAuthors.name
} | params: { externalId: ${externalId} };`,
);
try {
const { account } = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
if (!account) {
throw new AccountNotFoundError(
`account not found. externalId: ${externalId}`,
);
}
const authorUsers = await this.usersRepository.findAuthorUsers(
context,
account.id,
);
const authors = authorUsers.map((x) => {
if (!x.author_id) {
throw new Error(
`author_id is Not Found. externalId: ${x.external_id}, userId: ${x.id}`,
);
}
return {
id: x.id,
authorId: x.author_id,
};
});
return authors;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getAuthors.name}`,
);
}
}
/**
* パートナーを追加する
* @param companyName パートナーの会社名
* @param country パートナーの所属する国
* @param email パートナーの管理者のメールアドレス
* @param adminName パートナーの管理者の名前
* @param creatorUserId パートナーを作成する上位階層アカウントのユーザーID
* @param creatorAccountTier パートナーを作成する上位階層アカウントの階層
*/
async createPartnerAccount(
context: Context,
companyName: string,
country: string,
email: string,
adminName: string,
creatorUserId: string,
creatorAccountTier: number,
): Promise<{ accountId: number }> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.createPartnerAccount.name
} | params: { creatorUserId: ${creatorUserId}, creatorAccountTier: ${creatorAccountTier} };`,
);
try {
let creatorAccountId: number;
let creatorAccountName: string | null;
try {
const creatorAccount = await this.usersRepository.findUserByExternalId(
context,
creatorUserId,
);
creatorAccountId = creatorAccount.account_id;
// メール送信処理で使用するため、追加パートナーの上位階層アカウントの会社名を取得する
creatorAccountName = creatorAccount.account?.company_name ?? null;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof UserNotFoundError) {
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
} else {
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
const ramdomPassword = makePassword();
let externalUser: { sub: string } | ConflictError;
try {
// 管理者ユーザを作成し、AzureADB2C IDを取得する
externalUser = await this.adB2cService.createUser(
context,
email,
ramdomPassword,
adminName,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// メールアドレスが重複していた場合はエラーを返す
if (isConflictError(externalUser)) {
throw new HttpException(
makeErrorResponse('E010301'),
HttpStatus.BAD_REQUEST,
);
}
let account: Account;
let user: User;
try {
// アカウントと管理者をセットで作成
const { newAccount, adminUser } =
await this.accountRepository.createAccount(
context,
companyName,
country,
creatorAccountId,
creatorAccountTier + 1,
externalUser.sub,
USER_ROLES.NONE,
undefined,
undefined,
);
account = newAccount;
user = adminUser;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.error(
`[${context.getTrackingId()}] create partner account failed`,
);
//リカバリ処理
// idpのユーザーを削除
await this.deleteAdB2cUser(externalUser.sub, context);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// 新規作成アカウント用のBlobコンテナを作成
await this.blobStorageService.createContainer(
context,
account.id,
country,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.error(
`[${context.getTrackingId()}] create partner container failed`,
);
//リカバリ処理
// idpのユーザーを削除
await this.deleteAdB2cUser(externalUser.sub, context);
// DBのアカウントとユーザーを削除
await this.deleteAccount(account.id, user.id, context);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// メール送信処理
try {
if (creatorAccountName === null) {
throw new Error(
`Account is not found. creatorUserId: ${creatorUserId}`,
);
}
await this.sendgridService.sendMailWithU114(
context,
account.id,
user.id,
email,
creatorAccountName,
);
return { accountId: account.id };
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.error(
`[${context.getTrackingId()}] create partner account send mail failed`,
);
//リカバリ処理
// idpのユーザーを削除
await this.deleteAdB2cUser(externalUser.sub, context);
// DBのアカウントを削除
await this.deleteAccount(account.id, user.id, context);
// Blobコンテナを削除
await this.deleteBlobContainer(account.id, country, context);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} catch (e) {
throw e;
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.createPartnerAccount.name}`,
);
}
}
/**
* パートナーライセンス情報を取得する
* @param limit
* @param offset
* @param accountId
* @returns getPartnerLicensesResponse
*/
async getPartnerLicenses(
context: Context,
limit: number,
offset: number,
accountId: number,
): Promise<GetPartnerLicensesResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getPartnerLicenses.name
} | params: { limit: ${limit}, offset: ${offset}, accountId: ${accountId} };`,
);
try {
const currentDate = new DateWithZeroTime();
// 第五階層のshortage算出に使用する日付情報
// 「有効期限が現在日付からしきい値以内のライセンス数」を取得するため、しきい値となる日付を作成する
const expiringSoonDate = new ExpirationThresholdDate(
currentDate.getTime(),
);
const getPartnerLicenseResult =
await this.accountRepository.getPartnerLicense(
context,
accountId,
currentDate,
expiringSoonDate,
offset,
limit,
);
// 自アカウントのShortageを算出してreturn用の変数にマージする
let ownShortage =
getPartnerLicenseResult.ownPartnerLicenseFromRepository.stockLicense -
getPartnerLicenseResult.ownPartnerLicenseFromRepository.issuedRequested;
// 「不足している値」を取得するため、負数の場合は絶対値とし、0以上の場合は0とする
ownShortage = ownShortage >= 0 ? 0 : Math.abs(ownShortage);
// return用の型にリポジトリから取得した型をマージし、不足項目shortageを設定する
const ownPartnerLicense: PartnerLicenseInfo = Object.assign(
{},
getPartnerLicenseResult.ownPartnerLicenseFromRepository,
{
shortage: ownShortage,
},
);
// 各子アカウントのShortageを算出してreturn用の変数にマージする
const childrenPartnerLicenses: PartnerLicenseInfo[] = [];
for (const childPartnerLicenseFromRepository of getPartnerLicenseResult.childPartnerLicensesFromRepository) {
const { allocatableLicenseWithMargin, expiringSoonLicense } =
childPartnerLicenseFromRepository;
let childShortage = 0;
if (childPartnerLicenseFromRepository.tier === TIERS.TIER5) {
if (
allocatableLicenseWithMargin === undefined ||
expiringSoonLicense === undefined
) {
throw new Error(
`Tier5 account has no allocatableLicenseWithMargin or expiringSoonLicense. accountId: ${accountId}`,
);
}
childShortage = allocatableLicenseWithMargin - expiringSoonLicense;
} else {
childShortage =
childPartnerLicenseFromRepository.stockLicense -
childPartnerLicenseFromRepository.issuedRequested;
}
// 「不足している値」を取得するため、負数の場合は絶対値とし、0以上の場合は0とする
childShortage = childShortage >= 0 ? 0 : Math.abs(childShortage);
const childPartnerLicense: PartnerLicenseInfo = Object.assign(
{},
childPartnerLicenseFromRepository,
{
shortage: childShortage,
},
);
childrenPartnerLicenses.push(childPartnerLicense);
}
const getPartnerLicensesResponse: GetPartnerLicensesResponse = {
total: getPartnerLicenseResult.total,
ownPartnerLicense: ownPartnerLicense,
childrenPartnerLicenses: childrenPartnerLicenses,
};
return getPartnerLicensesResponse;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getPartnerLicenses.name}`,
);
}
}
/**
* 注文履歴情報を取得する
* @param limit
* @param offset
* @param accountId
* @returns getOrderHistoriesResponse
*/
async getOrderHistories(
context: Context,
limit: number,
offset: number,
accountId: number,
): Promise<GetOrderHistoriesResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getOrderHistories.name
} | params: { limit: ${limit}, offset: ${offset}, accountId: ${accountId} };`,
);
try {
const licenseHistoryInfo =
await this.licensesRepository.getLicenseOrderHistoryInfo(
context,
accountId,
offset,
limit,
);
// 戻り値用に配列の詰めなおしを行う
const orderHistories: LicenseOrder[] = [];
for (const licenseOrder of licenseHistoryInfo.licenseOrders) {
const returnLicenseOrder: LicenseOrder = {
issueDate:
licenseOrder.issued_at !== null
? new Date(licenseOrder.issued_at).toISOString()
: undefined,
numberOfOrder: licenseOrder.quantity,
orderDate: new Date(licenseOrder.ordered_at).toISOString(),
poNumber: licenseOrder.po_number,
status: licenseOrder.status,
};
orderHistories.push(returnLicenseOrder);
}
const getOrderHistoriesResponse: GetOrderHistoriesResponse = {
total: licenseHistoryInfo.total,
orderHistories: orderHistories,
};
return getOrderHistoriesResponse;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getOrderHistories.name}`,
);
}
}
/**
* 対象の注文を発行する
* @param context
* @param orderedAccountId
* @param userId
* @param tier
* @param poNumber
*/
async issueLicense(
context: Context,
orderedAccountId: number,
userId: string,
tier: number,
poNumber: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.issueLicense.name
} | params: { ` +
`orderedAccountId: ${orderedAccountId}, ` +
`userId: ${userId}, ` +
`tier: ${tier}, ` +
`poNumber: ${poNumber} };`,
);
try {
// アクセストークンからユーザーIDを取得する
const myAccountId = (
await this.usersRepository.findUserByExternalId(context, userId)
).account_id;
const { issuedOrderId } = await this.licensesRepository.issueLicense(
context,
orderedAccountId,
myAccountId,
tier,
poNumber,
);
try {
// 発行済みの注文をID指定して取得する
const orderLicense = await this.licensesRepository.getLicenseOrder(
context,
orderedAccountId,
poNumber,
issuedOrderId,
);
if (orderLicense == null) {
throw new Error(
`issue target order not found. fromAccountId: ${orderedAccountId}, poNumber:${poNumber}`,
);
}
// 未発行の注文の場合、エラー
if (orderLicense.status !== LICENSE_ISSUE_STATUS.ISSUED) {
throw new AlreadyIssuedError(
`An order for PONumber:${poNumber} has not been issued.`,
);
}
// 注文したアカウントと自分のアカウントの情報を取得
const customer = await this.getAccountInformation(
context,
orderedAccountId,
);
const dealer = await this.getAccountInformation(context, myAccountId);
await this.sendgridService.sendMailWithU107(
context,
customer.adminEmails,
customer.companyName,
orderLicense.quantity,
poNumber,
dealer.adminEmails,
dealer.companyName,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// メール送信に関する例外はログだけ出して握りつぶす
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case OrderNotFoundError:
throw new HttpException(
makeErrorResponse('E010801'),
HttpStatus.BAD_REQUEST,
);
case AlreadyIssuedError:
throw new HttpException(
makeErrorResponse('E010803'),
HttpStatus.BAD_REQUEST,
);
case LicensesShortageError:
throw new HttpException(
makeErrorResponse('E010804'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.issueLicense.name}`,
);
}
}
// dealersのアカウント情報を取得する
async getDealers(context: Context): Promise<GetDealersResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.getDealers.name}`,
);
try {
const dealerAccounts = await this.accountRepository.findDealerAccounts(
context,
);
const dealers: GetDealersResponse = {
dealers: dealerAccounts.map((dealerAccount): Dealer => {
return {
id: dealerAccount.id,
name: dealerAccount.company_name,
country: dealerAccount.country,
};
}),
};
return dealers;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getDealers.name}`,
);
}
}
/**
* タイピストグループを作成する
* @param context
* @param externalId
* @param typistGroupName
* @param typistIds
* @returns createTypistGroupResponse
**/
async createTypistGroup(
context: Context,
externalId: string,
typistGroupName: string,
typistIds: number[],
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.createTypistGroup.name
} | params: { ` +
`externalId: ${externalId}, ` +
`typistGroupName: ${typistGroupName}, ` +
`typistIds: ${typistIds} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id } = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
// API実行ユーザーのアカウントIDでタイピストグループを作成し、タイピストグループとtypistIdsのユーザーを紐付ける
await this.userGroupsRepository.createTypistGroup(
context,
typistGroupName,
typistIds,
account_id,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TypistIdInvalidError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
// 同名のタイピストグループが存在する場合は400エラーを返す
case TypistGroupNameAlreadyExistError:
throw new HttpException(
makeErrorResponse('E010909'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.createTypistGroup.name}`,
);
}
}
/**
* タイピストグループを更新する
* @param context
* @param externalId
* @param typistGroupId
* @param typistGroupName
* @param typistIds
* @returns typist group
*/
async updateTypistGroup(
context: Context,
externalId: string,
typistGroupId: number,
typistGroupName: string,
typistIds: number[],
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.updateTypistGroup.name
} | params: { ` +
`externalId: ${externalId}, ` +
`typistGroupId: ${typistGroupId}, ` +
`typistGroupName: ${typistGroupName}, ` +
`typistIds: ${typistIds} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id } = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
// タイピストグループと所属するタイピストを更新する
await this.userGroupsRepository.updateTypistGroup(
context,
account_id,
typistGroupId,
typistGroupName,
typistIds,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
// タイピストIDが存在しない場合は400エラーを返す
case TypistIdInvalidError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
// タイピストグループIDが存在しない場合は400エラーを返す
case TypistGroupNotExistError:
throw new HttpException(
makeErrorResponse('E010908'),
HttpStatus.BAD_REQUEST,
);
// 同名のタイピストグループが存在する場合は400エラーを返す
case TypistGroupNameAlreadyExistError:
throw new HttpException(
makeErrorResponse('E010909'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.updateTypistGroup.name}`,
);
}
}
/**
* タイピストグループを削除する
* @param context
* @param externalId
* @param typistGroupId
* @returns typist group
*/
async deleteTypistGroup(
context: Context,
externalId: string,
typistGroupId: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.deleteTypistGroup.name
} | params: { ` +
`externalId: ${externalId}, ` +
`typistGroupId: ${typistGroupId}, `,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id } = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
// タイピストグループを削除する
await this.userGroupsRepository.deleteTypistGroup(
context,
account_id,
typistGroupId,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
// タイピストグループ削除済み
case TypistGroupNotExistError:
throw new HttpException(
makeErrorResponse('E015001'),
HttpStatus.BAD_REQUEST,
);
// タイピストグループがルーティングルールに使用されている
case AssignedWorkflowDeleteFailedError:
throw new HttpException(
makeErrorResponse('E015002'),
HttpStatus.BAD_REQUEST,
);
// タイピストグループがタスクの文字起こし候補に使用されている
case ExistsCheckoutPermissionDeleteFailedError:
throw new HttpException(
makeErrorResponse('E015003'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.deleteTypistGroup.name}`,
);
}
}
/**
* ライセンス発行をキャンセルする
* @param context
* @param extarnalId
* @param poNumber
* @param orderedAccountId
*/
async cancelIssue(
context: Context,
extarnalId: string,
poNumber: string,
orderedAccountId: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.cancelIssue.name
} | params: { ` +
`extarnalId: ${extarnalId}, ` +
`poNumber: ${poNumber}, ` +
`orderedAccountId: ${orderedAccountId}, };`,
);
let myAccountId: number;
try {
// ユーザIDからアカウントIDを取得する
myAccountId = (
await this.usersRepository.findUserByExternalId(context, extarnalId)
).account_id;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
switch (e.constructor) {
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.cancelIssue.name}`,
);
}
// 注文元アカウントIDの親世代を取得
const parentAccountIds = await this.accountRepository.getHierarchyParents(
context,
orderedAccountId,
);
// 自身が存在しない場合、エラー
if (!parentAccountIds.includes(myAccountId)) {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.cancelIssue.name}`,
);
throw new HttpException(
makeErrorResponse('E000108'),
HttpStatus.UNAUTHORIZED,
);
}
try {
// 発行キャンセル処理
const { canceledIssueLicenseOrderId } =
await this.accountRepository.cancelIssue(
context,
orderedAccountId,
poNumber,
);
try {
// 発行キャンセルされ、発行済状態から注文中状態に戻った注文を取得する
const order = await this.licensesRepository.getLicenseOrder(
context,
orderedAccountId,
poNumber,
canceledIssueLicenseOrderId,
);
if (order == null) {
throw new Error('order not found.');
}
const { quantity, from_account_id, to_account_id } = order;
const customer = await this.getAccountInformation(
context,
from_account_id,
);
const dealer = await this.getAccountInformation(context, to_account_id);
await this.sendgridService.sendMailWithU109(
context,
dealer.adminEmails,
dealer.companyName,
quantity,
poNumber,
customer.adminEmails,
customer.companyName,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// メール送信の例外はログだけ出して握りつぶす
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
switch (e.constructor) {
case AlreadyLicenseStatusChangedError:
throw new HttpException(
makeErrorResponse('E010809'),
HttpStatus.BAD_REQUEST,
);
case CancellationPeriodExpiredError:
throw new HttpException(
makeErrorResponse('E010810'),
HttpStatus.BAD_REQUEST,
);
case AlreadyLicenseAllocatedError:
throw new HttpException(
makeErrorResponse('E010811'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.cancelIssue.name}`,
);
}
}
/**
* ワークタイプ一覧を取得します
* @param context
* @param externalId
* @returns worktypes
*/
async getWorktypes(
context: Context,
externalId: string,
): Promise<GetWorktypesResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getWorktypes.name
} | params: { externalId: ${externalId} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
// ワークタイプ一覧とActiveWorktypeIDを取得する
const { worktypes, active_worktype_id } =
await this.worktypesRepository.getWorktypes(context, accountId);
return {
worktypes: worktypes.map((x) => ({
id: x.id,
worktypeId: x.custom_worktype_id,
description: x.description ?? undefined,
})),
active: active_worktype_id,
};
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getWorktypes.name}`,
);
}
}
/**
* ワークタイプを作成します
* @param context
* @param externalId
* @param worktypeId
* @param [description]
* @returns worktype
*/
async createWorktype(
context: Context,
externalId: string,
worktypeId: string,
description?: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.createWorktype.name
} | params: { externalId: ${externalId}, worktypeId: ${worktypeId}, description: ${description} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
await this.worktypesRepository.createWorktype(
context,
accountId,
worktypeId,
description,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
// WorktypeIDが既に存在する場合は400エラーを返す
case WorktypeIdAlreadyExistsError:
throw new HttpException(
makeErrorResponse('E011001'),
HttpStatus.BAD_REQUEST,
);
// WorktypeIDが登録上限以上の場合は400エラーを返す
case WorktypeIdMaxCountError:
throw new HttpException(
makeErrorResponse('E011002'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.createWorktype.name}`,
);
}
}
/**
* ワークタイプを更新します
* @param context
* @param externalId
* @param id ワークタイプの内部ID
* @param worktypeId ユーザーが設定するワークタイプ名
* @param [description]
* @returns worktype
*/
async updateWorktype(
context: Context,
externalId: string,
id: number,
worktypeId: string,
description?: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.updateWorktype.name
} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id}, ` +
`worktypeId: ${worktypeId}, ` +
`description: ${description} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
// ワークタイプを更新する
await this.worktypesRepository.updateWorktype(
context,
accountId,
id,
worktypeId,
description,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
// ユーザーが設定したWorktypeIDが既存WorktypeのWorktypeIDと重複する場合は400エラーを返す
case WorktypeIdAlreadyExistsError:
throw new HttpException(
makeErrorResponse('E011001'),
HttpStatus.BAD_REQUEST,
);
// 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
case WorktypeIdNotFoundError:
throw new HttpException(
makeErrorResponse('E011003'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.updateWorktype.name}`,
);
}
}
/**
* ワークタイプを削除します
* @param context
* @param externalId
* @param id
* @returns worktype
*/
async deleteWorktype(
context: Context,
externalId: string,
id: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.deleteWorktype.name
} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account, account_id: accountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
if (!account) {
throw new AccountNotFoundError(
`account not found. externalId: ${externalId}`,
);
}
// ワークタイプを削除する
await this.worktypesRepository.deleteWorktype(context, accountId, id);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
// 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
case WorktypeIdNotFoundError:
throw new HttpException(
makeErrorResponse('E011003'),
HttpStatus.BAD_REQUEST,
);
// 内部IDで指定されたWorktypeがWorkflowで使用中の場合は400エラーを返す
case WorktypeIdInUseError:
throw new HttpException(
makeErrorResponse('E011004'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.deleteWorktype.name}`,
);
}
}
/**
* ワークタイプに紐づいたオプションアイテム一覧を取得します
* @param context
* @param externalId
* @param id Worktypeの内部ID
* @returns option items
*/
async getOptionItems(
context: Context,
externalId: string,
id: number,
): Promise<GetOptionItemsResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getOptionItems.name
} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
// オプションアイテム一覧を取得する
const optionItems = await this.worktypesRepository.getOptionItems(
context,
accountId,
id,
);
return {
optionItems: optionItems.map((x) => ({
id: x.id,
itemLabel: x.item_label,
defaultValueType: x.default_value_type,
initialValue: x.initial_value,
})),
};
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
// 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
case WorktypeIdNotFoundError:
throw new HttpException(
makeErrorResponse('E011003'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getOptionItems.name}`,
);
}
}
/**
* オプションアイテムを更新します
* @param context
* @param externalId
* @param id ワークタイプの内部ID
* @param optionItems
* @returns option items
*/
async updateOptionItems(
context: Context,
externalId: string,
id: number,
optionItems: PostWorktypeOptionItem[],
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.updateOptionItems.name
} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id}, ` +
`optionItems: ${JSON.stringify(optionItems)} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
// オプションアイテムを更新する
await this.worktypesRepository.updateOptionItems(
context,
accountId,
id,
// initialValueはdefaultValueTypeがDEFAULTの場合以外は空文字を設定する
optionItems.map((item) => ({
itemLabel: item.itemLabel,
defaultValueType: item.defaultValueType,
initialValue:
item.defaultValueType === OPTION_ITEM_VALUE_TYPE.DEFAULT
? item.initialValue
: '',
})),
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
// 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
case WorktypeIdNotFoundError:
throw new HttpException(
makeErrorResponse('E011003'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.updateOptionItems.name}`,
);
}
}
/**
* ActiveWorktypeの更新
* @param context
* @param externalId
* @param id ActiveWorktypeの内部ID
* @returns active worktype
*/
async updateActiveWorktype(
context: Context,
externalId: string,
id: number | undefined,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.updateActiveWorktype.name
} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
// ActiveWorktypeを更新する
await this.accountRepository.updateActiveWorktypeId(
context,
accountId,
id,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
// 内部IDで指定されたWorktypeが存在しない場合は400エラーを返す
case WorktypeIdNotFoundError:
throw new HttpException(
makeErrorResponse('E011003'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.updateActiveWorktype.name}`,
);
}
}
/**
* パートナー一覧を取得します
* @param context
* @param externalId
* @param limit
* @param offset
* @returns GetPartnersResponse
*/
async getPartners(
context: Context,
externalId: string,
limit: number,
offset: number,
): Promise<GetPartnersResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getPartners.name
} | params: { ` +
`externalId: ${externalId}, ` +
`limit: ${limit}, ` +
`offset: ${offset}, };`,
);
try {
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
const partnersRecords = await this.accountRepository.getPartners(
context,
accountId,
limit,
offset,
);
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
let externalIds = partnersRecords.partnersInfo.map(
(x) => x.primaryAccountExternalId,
);
externalIds = externalIds.filter((item) => item !== undefined);
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
// DBから取得した情報とADB2Cから取得した情報をマージ
const partners = partnersRecords.partnersInfo.map((db): Partner => {
const adb2cUser = adb2cUsers.find(
(adb2c) => db.primaryAccountExternalId === adb2c.id,
);
if (!adb2cUser) {
throw new Error(
`adb2c user not found. externalId: ${db.primaryAccountExternalId}`,
);
}
const { displayName: primaryAdmin, emailAddress: mail } =
getUserNameAndMailAddress(adb2cUser);
if (!mail) {
throw new Error(
`adb2c user mail not found. externalId: ${db.primaryAccountExternalId}`,
);
}
return {
name: db.name,
tier: db.tier,
accountId: db.accountId,
country: db.country,
primaryAdmin: primaryAdmin,
email: mail,
dealerManagement: db.dealerManagement,
};
});
return {
total: partnersRecords.total,
partners: partners,
};
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getPartners.name}`,
);
}
}
/**
* アカウント情報を設定する
* @param context
* @param externalId
* @param tier
* @param delegationPermission
* @param primaryAdminUserId
* @param parentAccountId
* @param secondryAdminUserId
* @returns UpdateAccountInfoResponse
*/
async updateAccountInfo(
context: Context,
externalId: string,
tier: number,
delegationPermission: boolean,
primaryAdminUserId: number,
parentAccountId?: number,
secondryAdminUserId?: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.updateAccountInfo.name
} | params: { ` +
`externalId: ${externalId}, ` +
`tier: ${tier}, ` +
`delegationPermission: ${delegationPermission}, ` +
`primaryAdminUserId: ${primaryAdminUserId}, ` +
`parentAccountId: ${parentAccountId}, ` +
`secondryAdminUserId: ${secondryAdminUserId}, };`,
);
try {
const { account_id: accountId, account } =
await this.usersRepository.findUserByExternalId(context, externalId);
await this.accountRepository.updateAccountInfo(
context,
accountId,
tier,
delegationPermission,
primaryAdminUserId,
parentAccountId,
secondryAdminUserId,
);
// メール送信処理
try {
if (account === null) {
throw new Error(`account not found. accountId: ${accountId}`);
}
let dealerName: string | null = null;
if (parentAccountId !== undefined) {
const dealer = await this.accountRepository.findAccountById(
context,
parentAccountId,
);
dealerName = dealer.company_name;
}
const { external_id: externalId } =
await this.usersRepository.findUserById(context, primaryAdminUserId);
const primaryAdmin = await this.adB2cService.getUser(
context,
externalId,
);
const {
displayName: primaryAdminName,
emailAddress: primaryAdminEmail,
} = getUserNameAndMailAddress(primaryAdmin);
if (!primaryAdminEmail) {
throw new Error(
`adb2c user mail not found. externalId: ${externalId}`,
);
}
await this.sendgridService.sendMailWithU112(
context,
primaryAdminName,
primaryAdminEmail,
account.company_name,
dealerName,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// メール送信に関する例外はログだけ出して握りつぶす
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case DealerAccountNotFoundError:
case AdminUserNotFoundError:
throw new HttpException(
makeErrorResponse('E010502'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.updateAccountInfo.name}`,
);
}
}
/**
* アカウントと紐づくデータを削除する
* @param context
* @param externalId
* @param accountId // 削除対象のアカウントID
*/
async deleteAccountAndData(
context: Context,
externalId: string,
accountId: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.deleteAccountAndData.name
} | params: { ` +
`externalId: ${externalId}, ` +
`accountId: ${accountId}, };`,
);
let country: string;
let dbUsers: User[];
// メール送信に必要な情報
let companyName: string | null = null;
let primaryAdminName: string | null = null;
let primaryAdminEmail: string | null = null;
try {
// パラメータとトークンから取得したアカウントIDの突き合わせ
const { account_id: myAccountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
if (myAccountId !== accountId) {
throw new HttpException(
makeErrorResponse('E000108'),
HttpStatus.UNAUTHORIZED,
);
}
// アカウント削除前に必要な情報を退避する
const targetAccount = await this.accountRepository.findAccountById(
context,
accountId,
);
// メール送信に必要な情報を取得する
try {
companyName = targetAccount.company_name;
if (!targetAccount.primary_admin_user_id) {
throw new Error(
`primary_admin_user_id not found. accountId: ${accountId}`,
);
}
const primaryAdmin = await this.usersRepository.findUserById(
context,
targetAccount.primary_admin_user_id,
);
const adb2cAdmin = await this.adB2cService.getUser(
context,
primaryAdmin.external_id,
);
const { displayName, emailAddress } =
getUserNameAndMailAddress(adb2cAdmin);
primaryAdminName = displayName;
primaryAdminEmail = emailAddress ?? null;
} catch (e) {
// メール送信に関する例外はログだけ出して握りつぶす
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
}
// 削除対象アカウントを削除する
dbUsers = await this.accountRepository.deleteAccountAndInsertArchives(
context,
accountId,
);
this.logger.log(
`[${context.getTrackingId()}] delete account: ${accountId}`,
);
country = targetAccount.country;
} catch (e) {
// アカウントの削除に失敗した場合はエラーを返す
this.logger.log(`[${context.getTrackingId()}] ${e}`);
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.deleteAccountAndData.name}`,
);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// 削除対象アカウント内のADB2Cユーザーをすべて削除する
await this.adB2cService.deleteUsers(
dbUsers.map((x) => x.external_id),
context,
);
this.logger.log(
`[${context.getTrackingId()}] delete ADB2C users: ${accountId}, users_id: ${dbUsers.map(
(x) => x.external_id,
)}`,
);
} catch (e) {
// ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行
this.logger.log(`[${context.getTrackingId()}] ${e}`);
this.logger.log(
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete ADB2C users: ${accountId}, users_id: ${dbUsers.map(
(x) => x.external_id,
)}`,
);
}
try {
// blobstorageコンテナを削除する
await this.deleteBlobContainer(accountId, country, context);
this.logger.log(
`[${context.getTrackingId()}] delete blob container: ${accountId}-${country}`,
);
} catch (e) {
// blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了
this.logger.log(`[${context.getTrackingId()}] ${e}`);
this.logger.log(
`${MANUAL_RECOVERY_REQUIRED}[${context.getTrackingId()}] Failed to delete blob container: ${accountId}, country: ${country}`,
);
}
// メール送信処理
try {
if (companyName === null) {
throw new Error('companyName is null');
}
if (primaryAdminName === null) {
throw new Error('primaryAdminName is null');
}
if (primaryAdminEmail === null) {
throw new Error('primaryAdminEmail is null');
}
await this.sendgridService.sendMailWithU111(
context,
primaryAdminName,
primaryAdminEmail,
companyName,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// メール送信に関する例外はログだけ出して握りつぶす
}
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.deleteAccountAndData.name}`,
);
}
/**
* IDトークンのsubからアカウントの階層情報を取得します
* @param context
* @param externalId
* @returns account info minimal access
*/
async getAccountInfoMinimalAccess(
context: Context,
externalId: string,
): Promise<number> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getAccountInfoMinimalAccess.name
} | params: { externalId: ${externalId} };`,
);
try {
const { account } = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
if (!account) {
throw new AccountNotFoundError(
`Account not found. externalId: ${externalId}`,
);
}
return account.tier;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${
this.getAccountInfoMinimalAccess.name
}`,
);
}
}
/**
* 自アカウントの会社名を取得する
* @param accountId
* @returns CompanyName
*/
async getCompanyName(
context: Context,
accountId: number,
): Promise<GetCompanyNameResponse> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getCompanyName.name
} | params: { accountId: ${accountId}, };`,
);
try {
const { company_name } = await this.accountRepository.findAccountById(
context,
accountId,
);
return { companyName: company_name };
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E010501'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getCompanyName.name}`,
);
}
}
/**
* アカウントIDを指定して、アカウント情報と管理者情報を取得する
* @param context
* @param accountId 対象アカウントID
* @returns 企業名/管理者メールアドレス
*/
private async getAccountInformation(
context: Context,
accountId: number,
): Promise<{
companyName: string;
adminEmails: string[];
}> {
// アカウントIDから企業名を取得する
const { company_name } = await this.accountRepository.findAccountById(
context,
accountId,
);
// 管理者一覧を取得
const admins = await this.usersRepository.findAdminUsers(
context,
accountId,
);
const adminExternalIDs = admins.map((x) => x.external_id);
// ADB2Cから管理者IDを元にメールアドレスを取得する
const usersInfo = await this.adB2cService.getUsers(
context,
adminExternalIDs,
);
// 生のAzure AD B2Cのユーザー情報からメールアドレスを抽出する
const adminEmails = usersInfo.map((x) => {
const { emailAddress } = getUserNameAndMailAddress(x);
if (emailAddress == null) {
throw new Error(`admin email-address is not found. id=${x.id}`);
}
return emailAddress;
});
return {
companyName: company_name,
adminEmails: adminEmails,
};
}
/**
* 自動ファイル削除に関する設定を更新する
* @param context
* @param externalId
* @param isAutoFileDelete // ファイルの自動削除可否
* @param retentionDays // ファイルの保持期間
*/
async updateFileDeleteSetting(
context: Context,
externalId: string,
isAutoFileDelete: boolean,
retentionDays: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.updateFileDeleteSetting.name
} | params: { ` +
`externalId: ${externalId}, ` +
`autoFileDelete: ${isAutoFileDelete}, ` +
`retentionDays: ${retentionDays}, };`,
);
// アカウントテーブルの更新を行う
try {
// externalIdを基に自アカウントの情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
await this.accountRepository.updateFileDeleteSetting(
context,
accountId,
isAutoFileDelete,
retentionDays,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// アカウントが存在しない場合のエラーもINTERNAL_SERVER_ERROR扱いのため個別の判定は行わない
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${
this.updateFileDeleteSetting.name
}`,
);
}
}
/**
* 指定したアカウントのシステム利用制限状態を更新する
* @param context
* @param accountId 更新対象アカウントID
* @param restricted 制限するかどうかtrue:制限する)
*/
async updateRestrictionStatus(
context: Context,
accountId: number,
restricted: boolean,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.updateRestrictionStatus.name
} | params: { ` +
`accountId: ${accountId}, ` +
`restricted: ${restricted},};`,
);
try {
await this.accountRepository.updateRestrictionStatus(
context,
accountId,
restricted,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// アカウントが存在しない場合のエラーもINTERNAL_SERVER_ERROR扱いのため個別の判定は行わない
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${
this.updateRestrictionStatus.name
}`,
);
}
}
/**
* 指定した子アカウントの親アカウントを、指定した親アカウントに変更する
* @param context
* @param newParent
* @param children
* @returns parent
*/
async switchParent(
context: Context,
newParent: number,
children: number[],
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.switchParent.name
} | params: { ` +
`newParent: ${newParent}, ` +
`children: ${children.join(', ')}};`,
);
try {
// 切り替え対象の情報取得
const childrenAccounts = await this.accountRepository.findAccountsById(
context,
children,
);
if (childrenAccounts.length !== children.length) {
// 指定された子アカウントが一つでも存在しない場合は通常運用ではありえないので汎用エラー
throw new Error('Some children accounts are not found');
}
const parentAccount = await this.accountRepository.findAccountById(
context,
newParent,
);
if (!parentAccount) {
// 指定された親アカウントが存在しない場合は通常運用で起こりうるため、BAD_REQUEST
throw new AccountNotFoundError(
`Parent account is not found. accountId=${newParent}`,
);
}
// 切り替え可否チェック(階層関係)
if (
!this.isValidHierarchyRelation(
parentAccount.tier,
childrenAccounts.map((x) => x.tier),
)
) {
throw new HierarchyMismatchError(
`Invalid hierarchy relation. parentAccount.tier=${parentAccount.tier}`,
);
}
// 切り替え可否チェック(リージョン・国関係)
const { success, errorType } = this.isValidLocationRelation(
parentAccount,
childrenAccounts,
);
if (!success) {
throw errorType;
}
// 切り替え処理実施
await this.accountRepository.switchParentAccount(
context,
newParent,
children,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case AccountNotFoundError:
throw new HttpException(
makeErrorResponse('E017001'),
HttpStatus.BAD_REQUEST,
);
case HierarchyMismatchError:
throw new HttpException(
makeErrorResponse('E017002'),
HttpStatus.BAD_REQUEST,
);
case RegionMismatchError:
throw new HttpException(
makeErrorResponse('E017003'),
HttpStatus.BAD_REQUEST,
);
case CountryMismatchError:
throw new HttpException(
makeErrorResponse('E017004'),
HttpStatus.BAD_REQUEST,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.switchParent.name}`,
);
}
}
/**
* 切り替え対象の親アカウントと子アカウントの階層関係が正しいかどうかをチェックする
* @param parentTier
* @param childrenTiers
* @returns true if valid hierarchy relation
*/
private isValidHierarchyRelation(
parentTier: number,
childrenTiers: number[],
): boolean {
// 全ての子アカウントの階層が、親アカウントの階層の一つ下であるかつ、第三<->第四または第四<->第五の切り替えの場合のみ判定OK。
if (
parentTier === TIERS.TIER3 &&
childrenTiers.every((child) => child === TIERS.TIER4)
) {
return true;
} else if (
parentTier === TIERS.TIER4 &&
childrenTiers.every((child) => child === TIERS.TIER5)
) {
return true;
}
return false;
}
/**
* 切り替え対象の親アカウントと子アカウントのリージョン・国関係が正しいかどうかをチェックする。
* @param parent
* @param children
* @returns valid location relation
*/
private isValidLocationRelation(
parent: Account,
children: Account[],
): {
success: boolean;
errorType: null | RegionMismatchError | CountryMismatchError;
} {
// 第三<->第四の切り替えはリージョンの一致を確認し、第四<->第五の切り替えは国の一致を確認する。
if (parent.tier === TIERS.TIER3) {
if (
!children.every(
(child) =>
this.getRegion(child.country) === this.getRegion(parent.country),
)
) {
return {
success: false,
errorType: new RegionMismatchError('Invalid region relation'),
};
}
return { success: true, errorType: null };
} else if (parent.tier === TIERS.TIER4) {
if (!children.every((child) => child.country === parent.country)) {
return {
success: false,
errorType: new CountryMismatchError('Invalid country relation'),
};
}
return { success: true, errorType: null };
} else {
// 親アカウントの階層が想定外の場合、本関数の使い方が間違っているので例外を投げる
throw new Error('Not implemented');
}
}
/**
* 国の所属する地域を取得する。
* @param country
* @returns region
*/
private getRegion(country: string): string {
// OMDS様より、地域はBlobStorageのリージョンで判定するでOKとのこと。
if (BLOB_STORAGE_REGION_AU.includes(country)) {
return 'AU';
} else if (BLOB_STORAGE_REGION_EU.includes(country)) {
return 'EU';
} else if (BLOB_STORAGE_REGION_US.includes(country)) {
return 'US';
} else {
// ここに到達する場合は、国が想定外の値であるため、例外を投げる
throw new Error(`Invalid country. country=${country}`);
}
}
}