水本 祐希 78cbfd15e8 Merged PR 400: API修正(アカウント情報取得API)
## 概要
[Task2601: API修正(アカウント情報取得API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2601)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
    - アカウント情報取得APIで返却する値を追加
    - テストしやすさを考慮し、getMyAccountInfoのパラメータと関数名を修正
    - ログ出力について規約に沿った形に修正

- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
アクセストークンを使ったユニットテストがあれば教えてください。

- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認
アクセストークンからアカウント情報を取得するAPIであるため、ポストマンで確認しました。
- 確認事項
  - 追加したtier、country、parentAccountId、delegationPermission、primaryAdminUserId、secondryAdminUserIdが返却されることを確認。
  - 異常系
    - MySQLにてusersとaccountsがない場合のエラーメッセージが返却されるかを確認
 

## 補足
- 相談、参考資料などがあれば
2023-09-15 02:17:54 +00:00

1557 lines
47 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,
} 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 { AccessToken } from '../../common/token';
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 } 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,
);
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,
},
};
} 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}`,
);
}
}
/**
* パートナー一覧を取得します
* @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}`);
}
}
}