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, 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, 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, WorktypeIdInUseError, WorktypeIdMaxCountError, WorktypeIdNotFoundError, } from '../../repositories/worktypes/errors/types'; @Injectable() export class AccountsService { private readonly mailFrom = this.configService.getOrThrow('MAIL_FROM'); 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( context: Context, accountId: number, ): Promise { 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 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(`[${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( 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 { // メールの内容を構成 const { subject, text, html } = await this.sendgridService.createMailContentFromEmailConfirm( context, account.id, user.id, email, ); // メールを送信 await this.sendgridService.sendMail( context, email, this.mailFrom, subject, text, html, ); } 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 { 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 { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.deleteAccount.name } | params: { accountId: ${accountId}, userId: ${userId} };`, ); try { await this.accountRepository.deleteAccount(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 { 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 { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.getAccountInfo.name } | params: { ` + `externalId: ${externalId}, };`, ); try { let userInfo: User; userInfo = await this.usersRepository.findUserByExternalId( context, externalId, ); let accountInfo: Account; 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, 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 { 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 { 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 { 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 { 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 myAccountId: number; try { // アクセストークンからユーザーIDを取得する myAccountId = ( await this.usersRepository.findUserByExternalId( context, creatorUserId, ) ).account_id; } 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( companyName, country, myAccountId, 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 { const { subject, text, html } = await this.sendgridService.createMailContentFromEmailConfirmForNormalUser( context, account.id, user.id, email, ); await this.sendgridService.sendMail( context, email, this.mailFrom, subject, text, html, ); 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 { 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: number = 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 { 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() .substring(0, 10) .replace(/-/g, '/') : undefined, numberOfOrder: licenseOrder.quantity, orderDate: new Date(licenseOrder.ordered_at) .toISOString() .substring(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(`[${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 { 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; await this.licensesRepository.issueLicense( context, orderedAccountId, myAccountId, tier, poNumber, ); } 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 { 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 { 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, ); 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 { 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, ); 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 extarnalId * @param poNumber * @param orderedAccountId */ async cancelIssue( context: Context, extarnalId: string, poNumber: string, orderedAccountId: number, ): Promise { 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 { // 発行キャンセル処理 await this.accountRepository.cancelIssue( context, orderedAccountId, poNumber, ); } 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 primaryAdmin = adb2cUser.displayName; const mail = adb2cUser.identities?.find( (identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS, )?.issuerAssignedId; 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 { 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 } = await this.usersRepository.findUserByExternalId(context, externalId); await this.accountRepository.updateAccountInfo( context, accountId, tier, delegationPermission, primaryAdminUserId, parentAccountId, secondryAdminUserId, ); } 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 { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.deleteAccountAndData.name } | params: { ` + `externalId: ${externalId}, ` + `accountId: ${accountId}, };`, ); let country: string; let dbUsers: User[]; 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, ); // 削除対象アカウントを削除する 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}`, ); } 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 { 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 { 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}`, ); } } }