import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; import { Context } from "../../common/log"; import { AccountsFileType, UsersFile, LicensesFile, WorktypesFile, csvInputFile, AccountsFile, } from "../../common/types/types"; import { COUNTRY_LIST, MIGRATION_TYPE, TIERS, WORKTYPE_MAX_COUNT, RECORDING_MODE, LICENSE_ALLOCATED_STATUS, USER_ROLES, SWITCH_FROM_TYPE, } from "src/constants"; import { registInputDataResponse, removeDuplicateEmailResponse, } from "./types/types"; import fs from "fs"; import { makeErrorResponse } from "src/common/error/makeErrorResponse"; @Injectable() export class TransferService { constructor() {} private readonly logger = new Logger(TransferService.name); /** * Transfer Input Data * @param OutputFilePath: string * @param csvInputFile: csvInputFile[] */ async transferInputData( context: Context, csvInputFile: csvInputFile[], accountIdMap: Map ): Promise { // パラメータ内容が長大なのでログには出さない this.logger.log( `[IN] [${context.getTrackingId()}] ${this.transferInputData.name}` ); try { let accountsFileTypeLines: AccountsFileType[] = []; let usersFileLines: UsersFile[] = []; let licensesFileLines: LicensesFile[] = []; let worktypesFileLines: WorktypesFile[] = []; let errorArray: string[] = []; let userIdIndex = 0; // authorIdとuserIdの対応関係を保持するMapを定義 const authorIdToUserIdMap: Map = new Map(); // countryのリストを生成 const countryAccounts = csvInputFile.filter( (item) => item.type === "Country" ); // csvInputFileを一行読み込みする csvInputFile.forEach((line) => { // typeが"USER"以外の場合、アカウントデータの作成を行う if (line.type !== MIGRATION_TYPE.USER) { // line.countryの値を読み込みCOUNTRY_LISTのlabelからvalueに変換する const country = COUNTRY_LIST.find( (country) => country.label === line.country )?.value; // adminNameの変換(last_name + " "+ first_name) // もしline.last_nameとline.first_nameが存在しない場合、line.admin_mailをnameにする let adminName = line.email; if (line.last_name && line.first_name) { adminName = `${line.last_name} ${line.first_name}`; // スペースが前後に入っている場合があるのでTrimする adminName = adminName.trim(); } // ランダムパスワードの生成(データ登録ツール側で行うのでやらない) // common/password/password.tsのmakePasswordを使用 // const autoGeneratedPassword = makePassword(); // parentAccountIdの設定 // parent_idが存在する場合、accountIdMapを参照し、accountIdに変換する let parentAccountId: number | null = null; if (line.parent_id) { parentAccountId = accountIdMap.get(line.parent_id); } // 万が一parent_idが入力されているのに存在しなかった場合は、エラー配列に追加する if (parentAccountId === undefined) { errorArray.push( `parent_id is invalid. parent_id=${line.parent_id}` ); } // userIdIndexをインクリメントする userIdIndex++; // AccountsFile配列にPush accountsFileTypeLines.push({ // accountIdはaccountIdMapから取得する accountId: accountIdMap.get(line.account_id), type: line.type, companyName: line.company_name, country: country, dealerAccountId: parentAccountId, adminName: adminName, adminMail: line.email, userId: userIdIndex, role: null, authorId: null, }); } else { // typeが"USER"の場合、かつcountryのアカウントIDに所属していない場合 if ( line.type == MIGRATION_TYPE.USER && !countryAccounts.some( (countryAccount) => countryAccount.account_id === line.account_id ) ) { // line.author_idが存在する場合のみユーザーデータを作成する if (line.author_id) { // userIdIndexをインクリメントする userIdIndex++; // nameの変換 // もしline.last_nameとline.first_nameが存在しない場合、line.emailをnameにする // 存在する場合は、last_name + " " + first_name let name = line.user_email; if (line.last_name && line.first_name) { name = `${line.last_name} ${line.first_name}`; } // UsersFileの作成 usersFileLines.push({ accountId: accountIdMap.get(line.account_id), userId: userIdIndex, name: name, role: USER_ROLES.AUTHOR, authorId: line.author_id, email: line.user_email, }); // authorIdとuserIdの対応関係をマッピング authorIdToUserIdMap.set(line.author_id, userIdIndex); } // ライセンスのデータの作成を行う // line.expired_dateが"9999/12/31 23:59:59"のデータの場合はデモライセンスなので登録しない if (line.expired_date !== "9999/12/31 23:59:59") { // authorIdが設定されてる場合、statusは"allocated"、allocated_user_idは対象のユーザID // されていない場合、statusは"reusable"、allocated_user_idはnull let status: string; let allocated_user_id: number | null; if (line.author_id) { status = LICENSE_ALLOCATED_STATUS.ALLOCATED; allocated_user_id = authorIdToUserIdMap.get(line.author_id) ?? null; // authorIdに対応するuserIdを取得 } else { status = LICENSE_ALLOCATED_STATUS.REUSABLE; allocated_user_id = null; } // LicensesFileの作成 licensesFileLines.push({ expiry_date: line.expired_date, account_id: accountIdMap.get(line.account_id), type: SWITCH_FROM_TYPE.NONE, status: status, allocated_user_id: allocated_user_id, }); } // WorktypesFileの作成 // wt1~wt20まで読み込み、account単位で作成する // 作成したWorktypesFileを配列にPush for (let i = 1; i <= WORKTYPE_MAX_COUNT; i++) { const wt = `wt${i}`; if (line[wt]) { // account_idで同一のcustom_worktype_idが存在しない場合は、作成する if ( !worktypesFileLines.find( (worktype) => worktype.account_id === accountIdMap.get(line.account_id) && worktype.custom_worktype_id === line[wt] ) ) { worktypesFileLines.push({ account_id: accountIdMap.get(line.account_id), custom_worktype_id: line[wt], }); } else { continue; } } } } } }); // エラー配列に値が存在する場合はエラーファイルを出力する if (errorArray.length > 0) { const errorFileJson = JSON.stringify(errorArray); fs.writeFileSync(`error.json`, errorFileJson); throw new HttpException( `errorArray is invalid. errorArray=${errorArray}`, HttpStatus.BAD_REQUEST ); } return { accountsFileTypeLines, usersFileLines, licensesFileLines, worktypesFileLines, }; } 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.transferInputData.name}` ); } } /** * 階層の付け替えを行う * @param accountsFileType: AccountsFileType[] * @returns AccountsFile[] */ async relocateHierarchy( context: Context, accountsFileType: AccountsFileType[] ): Promise { // パラメータ内容が長大なのでログには出さない this.logger.log( `[IN] [${context.getTrackingId()}] ${this.relocateHierarchy.name}` ); try { const relocatedAccounts: AccountsFile[] = []; const dealerRecords: Map = new Map(); const countryAccounts = accountsFileType.filter( (item) => item.type === MIGRATION_TYPE.COUNTRY ); const notCountryAccounts = accountsFileType.filter( (item) => item.type !== MIGRATION_TYPE.COUNTRY ); notCountryAccounts.forEach((notCountryAccount) => { let assignDealerAccountId = notCountryAccount.dealerAccountId; // 親アカウントIDがcountryの場合、countryの親アカウントIDを設定する for (const countryAccount of countryAccounts) { if (countryAccount.accountId === notCountryAccount.dealerAccountId) { assignDealerAccountId = countryAccount.dealerAccountId; } } const assignType = this.getAccountType(notCountryAccount.type); const newAccount: AccountsFile = { accountId: notCountryAccount.accountId, type: assignType, companyName: notCountryAccount.companyName, country: notCountryAccount.country, dealerAccountId: assignDealerAccountId, adminName: notCountryAccount.adminName, adminMail: notCountryAccount.adminMail, userId: notCountryAccount.userId, role: notCountryAccount.role, authorId: notCountryAccount.authorId, }; relocatedAccounts.push(newAccount); }); return relocatedAccounts; } 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.relocateHierarchy.name}` ); } } // メソッド: アカウントタイプを数値に変換するヘルパー関数 private getAccountType(type: string): number { switch (type) { case MIGRATION_TYPE.ADMINISTRATOR: return TIERS.TIER1; case MIGRATION_TYPE.BC: return TIERS.TIER2; case MIGRATION_TYPE.DISTRIBUTOR: return TIERS.TIER3; case MIGRATION_TYPE.DEALER: return TIERS.TIER4; case MIGRATION_TYPE.CUSTOMER: return TIERS.TIER5; default: return 0; } } /** * JSONファイルの出力 * @param outputFilePath: string * @param accountsFile: AccountsFile[] * @param usersFile: UsersFile[] * @param licensesFile: LicensesFile[] * @param worktypesFile: WorktypesFile[] */ async outputJsonFile( context: Context, outputFilePath: string, accountsFile: AccountsFile[], usersFile: UsersFile[], licensesFile: LicensesFile[], worktypesFile: WorktypesFile[] ): Promise { // パラメータ内容が長大なのでログには出さない this.logger.log( `[IN] [${context.getTrackingId()}] ${this.outputJsonFile.name}` ); try { // JSONファイルの出力を行う // AccountsFile配列の出力 const accountsFileJson = JSON.stringify(accountsFile); fs.writeFileSync(`${outputFilePath}accounts.json`, accountsFileJson); // UsersFile const usersFileJson = JSON.stringify(usersFile); fs.writeFileSync(`${outputFilePath}users.json`, usersFileJson); // LicensesFile const licensesFileJson = JSON.stringify(licensesFile); fs.writeFileSync(`${outputFilePath}licenses.json`, licensesFileJson); // WorktypesFile const worktypesFileJson = JSON.stringify(worktypesFile); fs.writeFileSync(`${outputFilePath}worktypes.json`, worktypesFileJson); } 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.outputJsonFile.name}` ); } } /** * データのバリデーションチェック * @param csvInputFile: csvInputFile[] */ async validateInputData( context: Context, csvInputFile: csvInputFile[] ): Promise { // パラメータ内容が長大なのでログには出さない this.logger.log( `[IN] [${context.getTrackingId()}] ${this.validateInputData.name}` ); try { // エラー配列を定義 let errorArray: string[] = []; // アカウントに対するworktypeのMap配列を作成する const accountWorktypeMap = new Map(); // csvInputFileのバリデーションチェックを行う csvInputFile.forEach((line, index) => { // typeのバリデーションチェック if ( line.type !== MIGRATION_TYPE.ADMINISTRATOR && line.type !== MIGRATION_TYPE.BC && line.type !== MIGRATION_TYPE.COUNTRY && line.type !== MIGRATION_TYPE.DISTRIBUTOR && line.type !== MIGRATION_TYPE.DEALER && line.type !== MIGRATION_TYPE.CUSTOMER && line.type !== MIGRATION_TYPE.USER ) { throw new HttpException( `type is invalid. index=${index} type=${line.type}`, HttpStatus.BAD_REQUEST ); } // typeがUSER以外の場合で、countryがnullの場合エラー配列に格納する if (line.type !== MIGRATION_TYPE.USER) { if (!line.country) { // countryがnullの場合エラー配列に格納する errorArray.push(`country is null. index=${index}`); } } // countryのバリデーションチェック if (line.country) { if (!COUNTRY_LIST.find((country) => country.label === line.country)) { throw new HttpException( `country is invalid. index=${index} country=${line.country}`, HttpStatus.BAD_REQUEST ); } } // mailのバリデーションチェック // メールアドレスの形式が正しいかどうかのチェック const mailRegExp = /^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/; if (line.email) { if (!mailRegExp.test(line.email)) { throw new HttpException( `email is invalid. index=${index} email=${line.email}`, HttpStatus.BAD_REQUEST ); } } if (line.user_email) { if (!mailRegExp.test(line.user_email)) { throw new HttpException( `user_email is invalid. index=${index} user_email=${line.email}`, HttpStatus.BAD_REQUEST ); } } // recording_modeの値が存在する場合 if (line.recording_mode) { // recording_modeのバリデーションチェック if ( line.recording_mode !== RECORDING_MODE.DS2_QP && line.recording_mode !== RECORDING_MODE.DS2_SP && line.recording_mode !== RECORDING_MODE.DSS ) { throw new HttpException( `recording_mode is invalid. index=${index} recording_mode=${line.recording_mode}`, HttpStatus.BAD_REQUEST ); } } // worktypeの1アカウント20件上限チェック for (let i = 1; i <= WORKTYPE_MAX_COUNT; i++) { const wt = `wt${i}`; if (line[wt]) { if (accountWorktypeMap.has(line.account_id)) { const worktypes = accountWorktypeMap.get(line.account_id); // 重複している場合はPushしない if (worktypes?.includes(line[wt])) { continue; } else { worktypes?.push(line[wt]); } // 20件を超えたらエラー if (worktypes?.length > WORKTYPE_MAX_COUNT) { throw new HttpException( `worktype is over. index=${index} account_id=${line.account_id}`, HttpStatus.BAD_REQUEST ); } } else { accountWorktypeMap.set(line.account_id, [line[wt]]); } } } }); // エラー配列に値が存在する場合はエラーファイルを出力する if (errorArray.length > 0) { const errorFileJson = JSON.stringify(errorArray); fs.writeFileSync(`error.json`, errorFileJson); throw new HttpException( `errorArray is invalid. errorArray=${errorArray}`, HttpStatus.BAD_REQUEST ); } } 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.validateInputData.name}` ); } } /** * removeDuplicateEmail * @param accountsFileLines: AccountsFile[] * @param usersFileLines: UsersFile[] * @param licensesFileLines: LicensesFile[] * @returns registInputDataResponse */ async removeDuplicateEmail( context: Context, accountsFileLines: AccountsFile[], usersFileLines: UsersFile[], licensesFileLines: LicensesFile[] ): Promise { // パラメータ内容が長大なのでログには出さない this.logger.log( `[IN] [${context.getTrackingId()}] ${this.removeDuplicateEmail.name}` ); try { const newAccountsFileLines: AccountsFile[] = []; const newUsersFileLines: UsersFile[] = []; const newLicensesFileLines: LicensesFile[] = [...licensesFileLines]; // licensesFileLinesを新規配列にコピー // accountsFileLinesの行ループ accountsFileLines.forEach((account) => { const duplicateAdminMail = newAccountsFileLines.find( (a) => a.adminMail.toLowerCase() === account.adminMail.toLowerCase() // メールアドレスは大文字小文字を区別しない ); if (duplicateAdminMail) { // 重複がある場合はどちらが取込対象か判断できないのでファイルを出力し、エラーにする const errorFileJson = JSON.stringify(account); fs.writeFileSync(`duplicate_error.json`, errorFileJson); throw new HttpException( `adminMail is duplicate. adminMail=${account.adminMail}`, HttpStatus.BAD_REQUEST ); } else { // 重複がない場合 newAccountsFileLines.push(account); } }); // usersFileLinesの行ループ usersFileLines.forEach((user) => { const duplicateUserEmail = newUsersFileLines.find( (u) => u.email.toLowerCase() === user.email.toLowerCase() // メールアドレスは大文字小文字を区別しない ); if (duplicateUserEmail) { // 重複がある場合 const index = newLicensesFileLines.findIndex( (license) => license.account_id === user.accountId && license.allocated_user_id === duplicateUserEmail.userId ); if (index !== -1) { // ライセンスの割り当てを解除 newLicensesFileLines[index].status = LICENSE_ALLOCATED_STATUS.REUSABLE; newLicensesFileLines[index].allocated_user_id = null; } } else { // 重複がない場合 newUsersFileLines.push(user); } // newAccountsFileLinesとの突合せ const duplicateAdminUserEmail = newAccountsFileLines.find( (a) => a.adminMail.toLowerCase() === user.email.toLowerCase() // メールアドレスは大文字小文字を区別しない ); // 重複がある場合 if (duplicateAdminUserEmail) { // 同一アカウント内での重複の場合 const isDuplicateInSameAccount = duplicateAdminUserEmail.accountId === user.accountId; if (isDuplicateInSameAccount) { // アカウント管理者にauthorロールを付与する duplicateAdminUserEmail.role = USER_ROLES.AUTHOR; duplicateAdminUserEmail.authorId = user.authorId; // アカウントにライセンスが割り当てられているか確認する const isAllocatedLicense = newLicensesFileLines.some( (license) => license.account_id === duplicateAdminUserEmail.accountId && license.allocated_user_id === duplicateAdminUserEmail.userId ); // 割り当てられていなければアカウントに割り当てる if (!isAllocatedLicense) { const index = newLicensesFileLines.findIndex( (license) => license.account_id === user.accountId && license.allocated_user_id === user.userId ); if (index !== -1) { newLicensesFileLines[index].allocated_user_id = duplicateAdminUserEmail.userId; } } } // ユーザーから割り当て解除する const index = newLicensesFileLines.findIndex( (license) => license.account_id === user.accountId && license.allocated_user_id === user.userId ); if (index !== -1) { // ライセンスの割り当てを解除 newLicensesFileLines[index].status = LICENSE_ALLOCATED_STATUS.REUSABLE; newLicensesFileLines[index].allocated_user_id = null; } // ユーザーの削除 const userIndex = newUsersFileLines.findIndex( (u) => u.userId === user.userId ); if (userIndex !== -1) { newUsersFileLines.splice(userIndex, 1); } } }); return { accountsFileLines: newAccountsFileLines, usersFileLines: newUsersFileLines, licensesFileLines: newLicensesFileLines, }; } 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.removeDuplicateEmail.name}` ); } } /** * transferDuplicateAuthor * @param accountsFileLines: AccountsFile[] * @param usersFileLines: UsersFile[] * @returns UsersFile[] */ async transferDuplicateAuthor( context: Context, accountsFileLines: AccountsFile[], usersFileLines: UsersFile[] ): Promise { // パラメータ内容が長大なのでログには出さない this.logger.log( `[IN] [${context.getTrackingId()}] ${this.transferDuplicateAuthor.name}` ); try { const newUsersFileLines: UsersFile[] = []; for (const accountsFileLine of accountsFileLines) { let duplicateSequence: number = 2; let authorIdList: String[] = []; // メールアドレス重複時はアカウントにもAuthorIdが設定されるので重複チェック用のリストに追加しておく if (accountsFileLine.authorId) { authorIdList.push(accountsFileLine.authorId); } const targetaccountUsers = usersFileLines.filter( (item) => item.accountId === accountsFileLine.accountId ); for (const targetaccountUser of targetaccountUsers) { let assignAuthorId = targetaccountUser.authorId; if (authorIdList.includes(targetaccountUser.authorId)) { // 同じauthorIdがいる場合、自分のauthorIdに連番を付与する assignAuthorId = assignAuthorId + duplicateSequence; duplicateSequence = duplicateSequence + 1; } authorIdList.push(targetaccountUser.authorId); // 新しいAuthorIdのユーザに詰め替え const newUser: UsersFile = { accountId: targetaccountUser.accountId, userId: targetaccountUser.userId, name: targetaccountUser.name, role: targetaccountUser.role, authorId: assignAuthorId, email: targetaccountUser.email, }; newUsersFileLines.push(newUser); } } return newUsersFileLines; } 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.transferDuplicateAuthor.name }` ); } } }