maruyama.t 664e815ef9 Merged PR 429: API実装(アカウント削除API:メイン処理)
## 概要
[Task2670: API実装(アカウント削除API:メイン処理)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2670)

アカウント削除APIを実装しました。
APIとしてはこれで実装完了ですが、DBに外部キー制約をつけていないので、現時点で削除できるものは以下のみです。
・アカウントテーブル
・ADB2Cのユーザー
・BLOBストレージ

## レビューポイント
内容が重めの処理なので全体的に見ていただけると嬉しいです。

## UIの変更
なし

## 動作確認状況
ローカルで以下の動作を確認
・RDBのアカウントが削除される
・ADB2Cのユーザーが削除される
・RDBのユーザーが退避テーブルに登録される
・BLOBストレージが削除される

## 補足
UTは別タスクに切り出しているので、本タスクでは実装していません。
2023-10-03 06:20:36 +00:00

1784 lines
54 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 { ConfigService } from '@nestjs/config';
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,
ADB2C_SIGN_IN_TYPE,
OPTION_ITEM_VALUE_TYPE,
MANUAL_RECOVERY_REQUIRED,
} from '../../constants';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import {
TypistGroup,
GetPartnerLicensesResponse,
PartnerLicenseInfo,
GetOrderHistoriesResponse,
LicenseOrder,
GetDealersResponse,
Dealer,
GetMyAccountResponse,
GetTypistGroupResponse,
GetWorktypesResponse,
GetOptionItemsResponse,
GetPartnersResponse,
PostWorktypeOptionItem,
} 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,
DealerAccountNotFoundError,
} 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 {
TypistGroupNotExistError,
TypistIdInvalidError,
} from '../../repositories/user_groups/errors/types';
import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service';
import {
WorktypeIdAlreadyExistsError,
WorktypeIdMaxCountError,
WorktypeIdNotFoundError,
} from '../../repositories/worktypes/errors/types';
@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 configService: ConfigService,
) {}
private readonly logger = new Logger(AccountsService.name);
/**
* 第五階層用のライセンス情報を取得する
* @param accountId
* @returns LicenseSummary
*/
async getLicenseSummary(
accountId: number,
): Promise<GetLicenseSummaryResponse> {
this.logger.log(`[IN] ${this.getLicenseSummary.name}`);
try {
const currentDate = new DateWithZeroTime();
const expiringSoonDate = new ExpirationThresholdDate(
currentDate.getTime(),
);
const { licenseSummary, isStorageAvailable } =
await this.accountRepository.getLicenseSummaryInfo(
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 licenseSummaryResponse: GetLicenseSummaryResponse = {
totalLicense,
allocatedLicense,
reusableLicense,
freeLicense,
expiringWithin14daysLicense: expiringSoonLicense,
issueRequesting,
numberOfRequesting,
storageSize: 0, // XXX PBI1201対象外
usedSize: 0, // XXX PBI1201対象外
shortage,
isStorageAvailable,
};
return licenseSummaryResponse;
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('get licenseSummary failed');
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* アカウント情報を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,
acceptedTermsVersion: string,
): Promise<{ accountId: number; userId: number; externalUserId: string }> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.createAccount.name} | params: { ` +
`country: ${country}, ` +
`dealerAccountId: ${dealerAccountId}, ` +
`role: ${role}, ` +
`acceptedTermsVersion: ${acceptedTermsVersion} };`,
);
try {
let externalUser: { sub: string } | ConflictError;
try {
// idpにユーザーを作成
externalUser = await this.adB2cService.createUser(
context,
email,
password,
username,
);
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('create externalUser failed');
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// メールアドレス重複エラー
if (isConflictError(externalUser)) {
this.logger.error(`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(
companyName,
country,
dealerAccountId,
TIERS.TIER5,
externalUser.sub,
role,
acceptedTermsVersion,
);
account = newAccount;
user = adminUser;
this.logger.log(
`[${context.trackingId}] adminUser.external_id: ${user.external_id}`,
);
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('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(`error=${e}`);
this.logger.error('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 {
// メールの送信元を取得
const from = this.configService.get<string>('MAIL_FROM') ?? '';
// メールの内容を構成
const { subject, text, html } =
await this.sendgridService.createMailContentFromEmailConfirm(
context,
account.id,
user.id,
email,
);
// メールを送信
await this.sendgridService.sendMail(
context,
email,
from,
subject,
text,
html,
);
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('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.trackingId}] ${this.createAccount.name}`,
);
}
}
// AdB2cのユーザーを削除
// TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
private async deleteAdB2cUser(
externalUserId: string,
context: Context,
): Promise<void> {
try {
await this.adB2cService.deleteUser(externalUserId, context);
this.logger.log(
`[${context.trackingId}] delete externalUser: ${externalUserId}`,
);
} catch (error) {
this.logger.error(`error=${error}`);
this.logger.error(
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
);
}
}
// DBのアカウントを削除
private async deleteAccount(
accountId: number,
userId: number,
context: Context,
): Promise<void> {
try {
await this.accountRepository.deleteAccount(accountId, userId);
this.logger.log(
`[${context.trackingId}] delete account: ${accountId}, user: ${userId}`,
);
} catch (error) {
this.logger.error(`error=${error}`);
this.logger.error(
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete account: ${accountId}, user: ${userId}`,
);
}
}
// Blobコンテナを削除
// TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補
private async deleteBlobContainer(
accountId: number,
country: string,
context: Context,
): Promise<void> {
try {
await this.blobStorageService.deleteContainer(
context,
accountId,
country,
);
this.logger.log(
`[${context.trackingId}] delete container: ${accountId}, country: ${country}`,
);
} catch (error) {
this.logger.error(
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete container: ${accountId}, country: ${country}`,
);
}
}
/**
* パラメータのユーザIDからアカウント情報を取得する
* @param externalId
* @returns GetMyAccountResponse
*/
async getAccountInfo(
context: Context,
externalId: string,
): Promise<GetMyAccountResponse> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.getAccountInfo.name} | params: { ` +
`name: ${externalId}, };`,
);
try {
let userInfo: User;
userInfo = await this.usersRepository.findUserByExternalId(externalId);
let accountInfo: Account;
accountInfo = await this.accountRepository.findAccountById(
userInfo.account_id,
);
let parentInfo: Account;
if (accountInfo.parent_account_id) {
parentInfo = await this.accountRepository.findAccountById(
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,
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.trackingId}] 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.trackingId}] ${this.getAccountInfo.name}`,
);
}
}
async getTypistGroups(externalId: string): Promise<TypistGroup[]> {
this.logger.log(`[IN] ${this.getTypistGroups.name}`);
// TypistGroup取得
try {
const user = await this.usersRepository.findUserByExternalId(externalId);
const userGroups = await this.userGroupsRepository.getUserGroups(
user.account_id,
);
return userGroups.map((x) => ({ id: x.id, name: x.name }));
} catch (e) {
this.logger.error(e);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(`[OUT] ${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.trackingId}] ${this.getTypistGroup.name} | params: { externalId: ${externalId}, typistGroupId: ${typistGroupId} };`,
);
try {
const { account_id } = await this.usersRepository.findUserByExternalId(
externalId,
);
const userGroup = await this.userGroupsRepository.getTypistGroup(
account_id,
typistGroupId,
);
return {
typistGroupName: userGroup.name,
typistIds: userGroup.userGroupMembers.map((x) => x.user_id),
};
} catch (e) {
this.logger.error(`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.trackingId}] ${this.getTypistGroup.name}`,
);
}
}
/**
* Gets typists
* @param externalId
* @returns typists
*/
async getTypists(externalId: string): Promise<Typist[]> {
this.logger.log(`[IN] ${this.getTypists.name}`);
// Typist取得
try {
const typistUsers = await this.usersRepository.findTypistUsers(
externalId,
);
const externalIds = typistUsers.map((x) => x.external_id);
// B2Cからユーザー名を取得する
const adb2cUsers = await this.adB2cService.getUsers(
// TODO: 外部連携以外のログ強化時に、ContollerからContextを取得するように修正する
{ trackingId: 'dummy' },
externalIds,
);
const typists = typistUsers.map((x) => {
const user = adb2cUsers.find((adb2c) => adb2c.id === x.external_id);
return {
id: x.id,
name: user.displayName,
};
});
return typists;
} catch (e) {
this.logger.error(e);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(`[OUT] ${this.getTypists.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.trackingId}] ${this.createPartnerAccount.name} | params: { creatorUserId: ${creatorUserId}, creatorAccountTier: ${creatorAccountTier} };`,
);
try {
let myAccountId: number;
try {
// アクセストークンからユーザーIDを取得する
myAccountId = (
await this.usersRepository.findUserByExternalId(creatorUserId)
).account_id;
} catch (e) {
this.logger.error(`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(`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(
companyName,
country,
myAccountId,
creatorAccountTier + 1,
externalUser.sub,
USER_ROLES.NONE,
null,
);
account = newAccount;
user = adminUser;
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('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(`error=${e}`);
this.logger.error('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 {
const from = this.configService.get<string>('MAIL_FROM') || '';
const { subject, text, html } =
await this.sendgridService.createMailContentFromEmailConfirmForNormalUser(
account.id,
user.id,
email,
);
await this.sendgridService.sendMail(
context,
email,
from,
subject,
text,
html,
);
return { accountId: account.id };
} catch (e) {
this.logger.error(`error=${e}`);
this.logger.error('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.trackingId}] ${this.createPartnerAccount.name}`,
);
}
}
/**
* パートナーライセンス情報を取得する
* @param limit
* @param offset
* @param accountId
* @returns getPartnerLicensesResponse
*/
async getPartnerLicenses(
limit: number,
offset: number,
accountId: number,
): Promise<GetPartnerLicensesResponse> {
this.logger.log(`[IN] ${this.getPartnerLicenses.name}`);
try {
const currentDate = new DateWithZeroTime();
// 第五階層のshortage算出に使用する日付情報
// 「有効期限が現在日付からしきい値以内のライセンス数」を取得するため、しきい値となる日付を作成する
const expiringSoonDate = new ExpirationThresholdDate(
currentDate.getTime(),
);
const getPartnerLicenseResult =
await this.accountRepository.getPartnerLicense(
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) {
let childShortage;
if (childPartnerLicenseFromRepository.tier === TIERS.TIER5) {
childShortage =
childPartnerLicenseFromRepository.allocatableLicenseWithMargin -
childPartnerLicenseFromRepository.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(e);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(`[OUT] ${this.getPartnerLicenses.name}`);
}
}
/**
* 注文履歴情報を取得する
* @param limit
* @param offset
* @param accountId
* @returns getOrderHistoriesResponse
*/
async getOrderHistories(
limit: number,
offset: number,
accountId: number,
): Promise<GetOrderHistoriesResponse> {
this.logger.log(`[IN] ${this.getOrderHistories.name}`);
try {
const licenseHistoryInfo =
await this.licensesRepository.getLicenseOrderHistoryInfo(
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()
.substr(0, 10)
.replace(/-/g, '/')
: null,
numberOfOrder: licenseOrder.quantity,
orderDate: new Date(licenseOrder.ordered_at)
.toISOString()
.substr(0, 10)
.replace(/-/g, '/'),
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(e);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(`[OUT] ${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.trackingId}] ${this.issueLicense.name} | params: { ` +
`orderedAccountId: ${orderedAccountId}, ` +
`userId: ${userId}, ` +
`tier: ${tier}, ` +
`poNumber: ${poNumber} };`,
);
try {
// アクセストークンからユーザーIDを取得する
const myAccountId = (
await this.usersRepository.findUserByExternalId(userId)
).account_id;
await this.licensesRepository.issueLicense(
orderedAccountId,
myAccountId,
tier,
poNumber,
);
} catch (e) {
this.logger.error(`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.trackingId}] ${this.issueLicense.name}`,
);
}
}
// dealersのアカウント情報を取得する
async getDealers(): Promise<GetDealersResponse> {
this.logger.log(`[IN] ${this.getDealers.name}`);
try {
const dealerAccounts = await this.accountRepository.findDealerAccounts();
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(e);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(`[OUT] ${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.trackingId}] ${this.createTypistGroup.name} | params: { ` +
`externalId: ${externalId}, ` +
`typistGroupName: ${typistGroupName}, ` +
`typistIds: ${typistIds} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id } = await this.usersRepository.findUserByExternalId(
externalId,
);
// API実行ユーザーのアカウントIDでタイピストグループを作成し、タイピストグループとtypistIdsのユーザーを紐付ける
await this.userGroupsRepository.createTypistGroup(
typistGroupName,
typistIds,
account_id,
);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TypistIdInvalidError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* タイピストグループを更新する
* @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.trackingId}] ${this.updateTypistGroup.name} | params: { typistGroupId: ${typistGroupId}, typistGroupName: ${typistGroupName}, typistIds: ${typistIds} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id } = await this.usersRepository.findUserByExternalId(
externalId,
);
// タイピストグループと所属するタイピストを更新する
await this.userGroupsRepository.updateTypistGroup(
account_id,
typistGroupId,
typistGroupName,
typistIds,
);
} catch (e) {
this.logger.error(`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,
);
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.trackingId}] ${this.updateTypistGroup.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.trackingId}] ${this.cancelIssue.name} | params: { ` +
`extarnalId: ${extarnalId}, ` +
`poNumber: ${poNumber}, ` +
`orderedAccountId: ${orderedAccountId}, };`,
);
let myAccountId: number;
try {
// ユーザIDからアカウントIDを取得する
myAccountId = (
await this.usersRepository.findUserByExternalId(extarnalId)
).account_id;
} catch (e) {
this.logger.error(`error=${e}`);
switch (e.constructor) {
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancelIssue.name}`);
}
// 注文元アカウントIDの親世代を取得
const parentAccountIds = await this.accountRepository.getHierarchyParents(
orderedAccountId,
);
// 自身が存在しない場合、エラー
if (!parentAccountIds.includes(myAccountId)) {
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancelIssue.name}`);
throw new HttpException(
makeErrorResponse('E000108'),
HttpStatus.UNAUTHORIZED,
);
}
try {
// 発行キャンセル処理
await this.accountRepository.cancelIssue(orderedAccountId, poNumber);
} catch (e) {
this.logger.error(`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.trackingId}] ${this.cancelIssue.name}`);
}
}
/**
* ワークタイプ一覧を取得します
* @param context
* @param externalId
* @returns worktypes
*/
async getWorktypes(
context: Context,
externalId: string,
): Promise<GetWorktypesResponse> {
this.logger.log(`[IN] [${context.trackingId}] ${this.getWorktypes.name}`);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
// ワークタイプ一覧とActiveWorktypeIDを取得する
const { worktypes, active_worktype_id } =
await this.worktypesRepository.getWorktypes(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(`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.trackingId}] ${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.trackingId}] ${this.createWorktype.name}`);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
await this.worktypesRepository.createWorktype(
accountId,
worktypeId,
description,
);
} catch (e) {
this.logger.error(`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.trackingId}] ${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.trackingId}] ${this.updateWorktype.name} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id}, ` +
`worktypeId: ${worktypeId}, ` +
`description: ${description} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
// ワークタイプを更新する
await this.worktypesRepository.updateWorktype(
accountId,
id,
worktypeId,
description,
);
} catch (e) {
this.logger.error(`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.trackingId}] ${this.updateWorktype.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.trackingId}] ${this.getOptionItems.name} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
// オプションアイテム一覧を取得する
const optionItems = await this.worktypesRepository.getOptionItems(
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(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.trackingId}] ${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.trackingId}] ${this.updateOptionItems.name} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id}, ` +
`optionItems: ${JSON.stringify(optionItems)} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
// オプションアイテムを更新する
await this.worktypesRepository.updateOptionItems(
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(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.trackingId}] ${this.updateOptionItems.name}`,
);
}
}
/**
* ActiveWorktypeの更新
* @param context
* @param externalId
* @param id ActiveWorktypeの内部ID
* @returns active worktype
*/
async updateActiveWorktype(
context: Context,
externalId: string,
id: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.updateActiveWorktype.name} | params: { ` +
`externalId: ${externalId}, ` +
`id: ${id} };`,
);
try {
// 外部IDをもとにユーザー情報を取得する
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
// ActiveWorktypeを更新する
await this.accountRepository.updateActiveWorktypeId(accountId, id);
} catch (e) {
this.logger.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.trackingId}] ${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.trackingId}] ${this.getPartners.name} | params: { ` +
`externalId: ${externalId}, ` +
`limit: ${limit}, ` +
`offset: ${offset}, };`,
);
try {
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
const partners = await this.accountRepository.getPartners(
accountId,
limit,
offset,
);
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
let externalIds = partners.partnersInfo.map(
(x) => x.primaryAccountExternalId,
);
externalIds = externalIds.filter((item) => item !== undefined);
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
// DBから取得した情報とADB2Cから取得した情報をマージ
const response = partners.partnersInfo.map((db) => {
const adb2cUser = adb2cUsers.find(
(adb2c) => db.primaryAccountExternalId === adb2c.id,
);
let primaryAdmin = undefined;
let mail = undefined;
if (adb2cUser) {
primaryAdmin = adb2cUser.displayName;
mail = adb2cUser.identities.find(
(identity) =>
identity.signInType === ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
).issuerAssignedId;
}
return {
name: db.name,
tier: db.tier,
accountId: db.accountId,
country: db.country,
primaryAdmin: primaryAdmin,
email: mail,
dealerManagement: db.dealerManagement,
};
});
return {
total: partners.total,
partners: response,
};
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(`[OUT] [${context.trackingId}] ${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.trackingId}] ${this.updateAccountInfo.name} | params: { ` +
`externalId: ${externalId}, ` +
`delegationPermission: ${delegationPermission}, ` +
`primaryAdminUserId: ${primaryAdminUserId}, ` +
`parentAccountId: ${parentAccountId}, ` +
`secondryAdminUserId: ${secondryAdminUserId}, };`,
);
try {
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(externalId);
await this.accountRepository.updateAccountInfo(
accountId,
tier,
delegationPermission,
primaryAdminUserId,
parentAccountId,
secondryAdminUserId,
);
} catch (e) {
this.logger.error(`[${context.trackingId}] 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.trackingId}] ${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.trackingId}] ${this.deleteAccountAndData.name} | params: { ` +
`externalId: ${externalId}, ` +
`accountId: ${accountId}, };`,
);
let country: string;
let dbUsers: User[];
try {
// パラメータとトークンから取得したアカウントIDの突き合わせ
const { account_id: myAccountId } =
await this.usersRepository.findUserByExternalId(externalId);
if (myAccountId !== accountId) {
throw new HttpException(
makeErrorResponse('E000108'),
HttpStatus.UNAUTHORIZED,
);
}
// アカウント削除前に必要な情報を退避する
const targetAccount = await this.accountRepository.findAccountById(
accountId,
);
// 削除対象アカウントを削除する
dbUsers = await this.accountRepository.deleteAccountAndInsertArchives(
accountId,
);
this.logger.log(`[${context.trackingId}] delete account: ${accountId}`);
country = targetAccount.country;
} catch (e) {
// アカウントの削除に失敗した場合はエラーを返す
this.logger.log(`[${context.trackingId}] ${e}`);
this.logger.log(
`[OUT] [${context.trackingId}] ${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.trackingId
}] delete ADB2C users: ${accountId}, users_id: ${dbUsers.map(
(x) => x.external_id,
)}`,
);
} catch (e) {
// ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行
this.logger.log(`[${context.trackingId}] ${e}`);
this.logger.log(
`${MANUAL_RECOVERY_REQUIRED} [${
context.trackingId
}] 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.trackingId}] delete blob container: ${accountId}-${country}`,
);
} catch (e) {
// blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了
this.logger.log(`[${context.trackingId}] ${e}`);
this.logger.log(
`${MANUAL_RECOVERY_REQUIRED}[${context.trackingId}] Failed to delete blob container: ${accountId}, country: ${country}`,
);
}
this.logger.log(
`[OUT] [${context.trackingId}] ${this.deleteAccountAndData.name}`,
);
}
}