import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; import { Context } from "../../common/log"; import { AccountsMappingFile, CardLicensesFile, csvInputFileWithRow, VerificationResultDetails, } from "../../common/types/types"; import { AUTO_INCREMENT_START, MIGRATION_TYPE, COUNTRY_LIST, } from "../../constants/index"; import { makeErrorResponse } from "../../common/error/makeErrorResponse"; import { LicensesRepositoryService } from "../../repositories/licenses/licenses.repository.service"; import { License, CardLicense, } from "../../repositories/licenses/entity/license.entity"; import { AccountsRepositoryService } from "../../repositories/accounts/accounts.repository.service"; import { UsersRepositoryService } from "../../repositories//users/users.repository.service"; import { Account } from "src/repositories/accounts/entity/account.entity"; import fs from "fs"; @Injectable() export class VerificationService { constructor( private readonly AccountsRepository: AccountsRepositoryService, private readonly UsersRepository: UsersRepositoryService, private readonly licensesRepository: LicensesRepositoryService ) {} private readonly logger = new Logger(VerificationService.name); /** * Verification Data * @param inputFilePath: string */ async varificationData( context: Context, inputFilePath: string, csvInputFiles: csvInputFileWithRow[], accountsMappingInputFiles: AccountsMappingFile[], cardlicensesInputFiles: CardLicensesFile[] ): Promise { // パラメータ内容が長大なのでログには出さない this.logger.log( `[IN] [${context.getTrackingId()}] ${this.varificationData.name}` ); // this.logger.log(csvInputFiles); try { // 件数情報の取得 this.logger.log(`入力ファイルから件数情報を取得する`); const accountFromFile = csvInputFiles.filter( (item) => item.type !== "USER" && item.type !== "Country" ); const accountCountFromFile = accountFromFile.length; const cardLicensesCountFromFile = cardlicensesInputFiles.length; const licensesCountFromFile = csvInputFiles.filter( (item) => item.type === "USER" && !item.expired_date.startsWith("9999/12/31") ).length + cardLicensesCountFromFile; // 管理ユーザ数のカウント const administratorCountFromFile = accountCountFromFile; // 一般ユーザ数のカウント // countryのアカウントに所属するユーザをカウント対象外とする const countryAccountFromFile = csvInputFiles.filter( (item) => item.type === "Country" ); // USER、かつuser_emailが設定なし、かつcountryのアカウントID以外をユーザとする const normaluserFromFile = csvInputFiles.filter( (item) => item.type === "USER" && item.user_email.length !== 0 && !countryAccountFromFile.some( (countryItem) => countryItem.account_id === item.account_id ) ); const normaluserCountFromFile = normaluserFromFile.length; // ユーザ重複数のカウント let mailAdresses: string[] = []; accountFromFile.forEach((item) => { // メールアドレスの要素を配列に追加 if (item.email.length !== 0) { mailAdresses.push(item.email); } }); normaluserFromFile.forEach((item) => { // メールアドレスの要素を配列に追加 if (item.user_email.length !== 0) { mailAdresses.push(item.user_email); } }); // 重複する要素を抽出 const duplicates: { [key: string]: number } = {}; mailAdresses.forEach((str) => { duplicates[str.toLowerCase()] = (duplicates[str.toLowerCase()] || 0) + 1; }); // 重複する要素と件数を表示 let duplicateCount = 0; Object.keys(duplicates).forEach((key) => { const count = duplicates[key]; if (count > 1) { // 重複件数をカウント duplicateCount = duplicateCount + (count - 1); //console.log(`${key}が${count}件`); } }); const userCountFromFile = administratorCountFromFile + normaluserCountFromFile - duplicateCount; this.logger.log(`accountCountFromFile=${accountCountFromFile}`); this.logger.log(`cardLicensesCountFromFile=${cardLicensesCountFromFile}`); this.logger.log(`licensesCountFromFile=${licensesCountFromFile}`); this.logger.log(`userCountFromFile=${userCountFromFile}`); // DBから情報を取得する this.logger.log(`DBの情報を取得する`); const accounts = await this.AccountsRepository.getAllAccounts(context); const users = await this.UsersRepository.getAllUsers(context); const licenses = await this.licensesRepository.getAllLicenses(context); const cardLicenses = await this.licensesRepository.getAllCardLicense( context ); // DB件数のカウント this.logger.log(`DBの情報から件数を取得する`); const accountsCountFromDB = accounts.length; const usersCountFromDB = users.length; const licensesCountFromDB = licenses.length; const cardLicensesCountFromDB = cardLicenses.length; this.logger.log(`accountsCountFromDB=${accountsCountFromDB}`); this.logger.log(`usersCountFromDB=${usersCountFromDB}`); this.logger.log(`licensesCountFromDB=${licensesCountFromDB}`); this.logger.log(`cardLicensesCountFromDB=${cardLicensesCountFromDB}`); // エラー情報の定義 const VerificationResultDetails: VerificationResultDetails[] = []; // カードライセンス関連の情報突き合わせ this.logger.log(`カードライセンス関連の情報突き合わせ`); const isCardDetailNoError = compareCardLicenses( VerificationResultDetails, cardlicensesInputFiles, cardLicenses, licenses ); // ライセンス関連の情報突き合わせ this.logger.log(`ライセンス関連の情報突き合わせ`); const isLicensesDetailNoError = compareLicenses( VerificationResultDetails, csvInputFiles.filter( (item) => item.type === "USER" && !item.expired_date.startsWith("9999/12/31") ), licenses.filter((item) => item.expiry_date !== null), accountsMappingInputFiles ); // アカウント情報の突き合わせ this.logger.log(`アカウント関連の情報突き合わせ`); const isAccountsDetailNoError = compareAccounts( VerificationResultDetails, csvInputFiles.filter( (item) => item.type !== "USER" && item.type !== "Country" ), csvInputFiles.filter((item) => item.type === "Country"), accounts, accountsMappingInputFiles ); // 結果の判定と出力 this.logger.log(`結果の判定と出力`); const isAccountCountNoDifference = accountCountFromFile === accountsCountFromDB; const isUsersCountNoDifference = userCountFromFile === usersCountFromDB; const isLicensesCountNoDifference = licensesCountFromFile === licensesCountFromDB; const isCardLicensesCountNoDifference = cardLicensesCountFromFile === cardLicensesCountFromDB; const isNoDetailError = VerificationResultDetails.length === 0; const isSummaryNoError = isAccountCountNoDifference && isUsersCountNoDifference && isLicensesCountNoDifference && isCardLicensesCountNoDifference && isNoDetailError; const summaryString = ` サマリファイル: 比較結果:${isSummaryNoError ? "OK" : "NG"} 件数: アカウント:${ isAccountCountNoDifference ? "OK" : "NG" }(csv件数:${accountCountFromFile}/DB件数:${accountsCountFromDB}) ライセンス:${ isLicensesCountNoDifference ? "OK" : "NG" }(csv件数:${licensesCountFromFile}/DB件数:${licensesCountFromDB}) カードライセンス:${ isCardLicensesCountNoDifference ? "OK" : "NG" }(csv件数:${cardLicensesCountFromFile}/DB件数:${cardLicensesCountFromDB}) ユーザー:${ isUsersCountNoDifference ? "OK" : "NG" }(csv件数:${userCountFromFile}/DB件数:${usersCountFromDB}) 項目比較: アカウント:${isAccountsDetailNoError ? "OK" : "NG"} カードライセンス:${isCardDetailNoError ? "OK" : "NG"} ライセンス:${isLicensesDetailNoError ? "OK" : "NG"} `; // サマリファイルの書き込み fs.writeFileSync(`${inputFilePath}resultsummary.txt`, summaryString); // 詳細ファイルの書き込み // 配列をJSON文字列に変換 const jsonContent = JSON.stringify(VerificationResultDetails, null, 2); // JSONをファイルに書き込み fs.writeFileSync(`${inputFilePath}resultdetail.json`, jsonContent); } 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.varificationData.name}` ); } } } // dateを任意のフォーマットに変換する const getFormattedDate = ( date: Date | null, format: string, padHours: boolean = false // trueの場合、hhについてゼロパディングする(00→0、01→1、23→23) ) => { if (!date) { return null; } const symbol = { M: date.getMonth() + 1, d: date.getDate(), h: date.getHours(), m: date.getMinutes(), s: date.getSeconds(), }; // hhの値をゼロパディングするかどうかのフラグを確認 const hourSymbol = padHours ? "hh" : "h"; const formatted = format.replace(/(M+|d+|h+|m+|s+)/g, (v) => ( (v.length > 1 && v !== hourSymbol ? "0" : "") + symbol[v.slice(-1) as keyof typeof symbol] ).slice(-2) ); return formatted.replace(/(y+)/g, (v) => date.getFullYear().toString().slice(-v.length) ); }; // 親の階層がcountryの場合、countryの親を返却する function transrateCountryHierarchy( countriesFromFile: csvInputFileWithRow[], targetParentAccountIdString: string ): string { for (const countryFromFile of countriesFromFile) { if (countryFromFile.account_id === targetParentAccountIdString) { return countryFromFile.parent_id; } } return targetParentAccountIdString; } // アカウントID(number)を対応するアカウントID(string)に変換する function findAccountIdText( accountsMappings: AccountsMappingFile[], targetAccountIdNumber: number ): string { if (targetAccountIdNumber == null) { return ""; } for (const accountsMapping of accountsMappings) { if (accountsMapping.accountIdNumber === targetAccountIdNumber) { return accountsMapping.accountIdText; } } return `NO_MATCHED_ACCOUNTID_${targetAccountIdNumber}`; // マッチするものが見つからない場合 } // 階層(number)を対応する階層(string)に変換する function getMigrationTypeByNumber(numberValue: number): string { switch (numberValue) { case 1: return MIGRATION_TYPE.ADMINISTRATOR; case 2: return MIGRATION_TYPE.BC; case 3: return MIGRATION_TYPE.DISTRIBUTOR; case 4: return MIGRATION_TYPE.DEALER; case 5: return MIGRATION_TYPE.CUSTOMER; default: return `NO_MATCHED_TIER_${numberValue}`; } } // 国(省略版)を対応する国(非省略版)に変換する function getCountryLabelByValue(value: string): string { const country = COUNTRY_LIST.find((country) => country.value === value); return country ? country.label : `NO_MATCHED_COUNTRY_${value}`; } // カードライセンス情報の突き合わせを行い、エラー時はエラー情報配列に情報追加する function compareCardLicenses( VerificationResultDetails: VerificationResultDetails[], cardlicensesInputFiles: CardLicensesFile[], cardLicenses: CardLicense[], licenses: License[] ): boolean { let isNoError = true; let row = 1; // カードライセンスファイルの行数 for (const cardlicensesInputFile of cardlicensesInputFiles) { const filterdCardLicenses = cardLicenses.filter( (cardLicenses) => cardLicenses.card_license_key === cardlicensesInputFile.card_license_key ); if (filterdCardLicenses.length === 0) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", inputRow: row, diffTargetTable: "cardLicenses", columnName: "card_license_key", fileData: cardlicensesInputFile.card_license_key, databaseData: "-", reason: "レコード無し", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; continue; } /* issue_idは自動採番のため比較しない if (cardlicensesInputFile.issue_id !== filterdCardLicenses[0].issue_id) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", diffTargetTable: "cardLicenses", columnName: "issue_id", fileData: cardlicensesInputFile.issue_id.toString(), databaseData: filterdCardLicenses[0].issue_id.toString(), reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; } */ const formattedActivated = getFormattedDate( filterdCardLicenses[0].activated_at, `yyyy/MM/dd hh:mm:ss` ); if (cardlicensesInputFile.activated_at !== formattedActivated) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", inputRow: row, diffTargetTable: "cardLicenses", columnName: "activated_at", fileData: cardlicensesInputFile.activated_at, databaseData: formattedActivated, reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; } const filterdLicenses = licenses.filter( (licenses) => licenses.id === filterdCardLicenses[0].license_id ); if (filterdLicenses.length === 0) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", inputRow: row, diffTargetTable: "licenses", columnName: "id", fileData: filterdCardLicenses[0].license_id.toString(), databaseData: "-", reason: "紐つくライセンスのレコード無し", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; continue; } if (filterdLicenses[0].expiry_date !== null) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", inputRow: row, diffTargetTable: "licenses", columnName: "expiry_date", fileData: null, databaseData: getFormattedDate( filterdLicenses[0].expiry_date, `yyyy/MM/dd hh:mm:ss` ), reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; } if (filterdLicenses[0].account_id !== AUTO_INCREMENT_START) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", inputRow: row, diffTargetTable: "licenses", columnName: "account_id", fileData: AUTO_INCREMENT_START.toString(), databaseData: filterdLicenses[0].account_id.toString(), reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; } if (filterdLicenses[0].type !== "CARD") { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", inputRow: row, diffTargetTable: "licenses", columnName: "type", fileData: "CARD", databaseData: filterdLicenses[0].type, reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; } if (filterdLicenses[0].status !== "Unallocated") { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", inputRow: row, diffTargetTable: "licenses", columnName: "status", fileData: "Unallocated", databaseData: filterdLicenses[0].status, reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; } if (filterdLicenses[0].allocated_user_id !== null) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", inputRow: row, diffTargetTable: "licenses", columnName: "allocated_user_id", fileData: null, databaseData: filterdLicenses[0].allocated_user_id.toString(), reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; } if (filterdLicenses[0].order_id !== null) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", inputRow: row, diffTargetTable: "licenses", columnName: "order_id", fileData: null, databaseData: filterdLicenses[0].order_id.toString(), reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; } if (filterdLicenses[0].deleted_at !== null) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", inputRow: row, diffTargetTable: "licenses", columnName: "deleted_at", fileData: null, databaseData: getFormattedDate( filterdLicenses[0].deleted_at, `yyyy/MM/dd hh:mm:ss` ), reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; } if (filterdLicenses[0].delete_order_id !== null) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "cardLicenses", inputRow: row, diffTargetTable: "licenses", columnName: "delete_order_id", fileData: null, databaseData: filterdLicenses[0].delete_order_id.toString(), reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; } row = row + 1; } return isNoError; } // ライセンス情報の突き合わせを行い、エラー時はエラー情報配列に情報追加する function compareLicenses( VerificationResultDetails: VerificationResultDetails[], licensesFromFile: csvInputFileWithRow[], licensesFromDatabase: License[], accountsMappingInputFiles: AccountsMappingFile[] ): boolean { let isNoError = true; for (let i = 0; i < licensesFromFile.length; i++) { if ( !licensesFromDatabase[i] || licensesFromFile[i].account_id !== findAccountIdText( accountsMappingInputFiles, licensesFromDatabase[i].account_id ) ) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "Account_transition", inputRow: licensesFromFile[i].row, diffTargetTable: "licenses", columnName: "account_id", fileData: licensesFromFile[i].account_id, databaseData: licensesFromDatabase[i] ? findAccountIdText( accountsMappingInputFiles, licensesFromDatabase[i].account_id ) + `(${licensesFromDatabase[i].account_id})` : "undifined", reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; } // expiry_dateについて、時はゼロパディングした値で比較する(×01~09 ○1~9) if ( !licensesFromDatabase[i] || licensesFromFile[i].expired_date !== getFormattedDate( licensesFromDatabase[i].expiry_date, `yyyy/MM/dd hh:mm:ss`, true ) ) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "Account_transition", inputRow: licensesFromFile[i].row, diffTargetTable: "licenses", columnName: "expired_date", fileData: licensesFromFile[i].expired_date, databaseData: licensesFromDatabase[i] ? getFormattedDate( licensesFromDatabase[i].expiry_date, `yyyy/MM/dd hh:mm:ss`, true ) : "undifined", reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; } } return isNoError; } // アカウント情報の突き合わせを行い、エラー時はエラー情報配列に情報追加する function compareAccounts( VerificationResultDetails: VerificationResultDetails[], accountsFromFile: csvInputFileWithRow[], countriesFromFile: csvInputFileWithRow[], accountsFromDatabase: Account[], accountsMappingInputFiles: AccountsMappingFile[] ): boolean { let isNoError = true; for (const accountFromFile of accountsFromFile) { // DBレコードの存在チェック const filterdAccounts = accountsFromDatabase.filter( (accountsFromDatabase) => findAccountIdText( accountsMappingInputFiles, accountsFromDatabase.id ) === accountFromFile.account_id ); if (filterdAccounts.length === 0) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "Account_transition", inputRow: accountFromFile.row, diffTargetTable: "accounts", columnName: "account_id", fileData: accountFromFile.account_id, databaseData: "-", reason: "レコード無し", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; continue; } // 項目チェック(parent_account_id) const transratedParentId = transrateCountryHierarchy( countriesFromFile, accountFromFile.parent_id ); if ( transratedParentId !== findAccountIdText( accountsMappingInputFiles, filterdAccounts[0].parent_account_id ) ) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "Account_transition", inputRow: accountFromFile.row, diffTargetTable: "accounts", columnName: "parent_account_id", fileData: transratedParentId === accountFromFile.parent_id ? accountFromFile.parent_id : `${transratedParentId}(${accountFromFile.parent_id})`, databaseData: findAccountIdText( accountsMappingInputFiles, filterdAccounts[0].parent_account_id ) + `(${filterdAccounts[0].parent_account_id})`, reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; continue; } // 項目チェック(tier) if ( accountFromFile.type !== getMigrationTypeByNumber(filterdAccounts[0].tier) ) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "Account_transition", inputRow: accountFromFile.row, diffTargetTable: "accounts", columnName: "tier", fileData: accountFromFile.type, databaseData: getMigrationTypeByNumber(filterdAccounts[0].tier), reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; continue; } // 項目チェック(country) if ( accountFromFile.country !== getCountryLabelByValue(filterdAccounts[0].country) ) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "Account_transition", inputRow: accountFromFile.row, diffTargetTable: "accounts", columnName: "country", fileData: accountFromFile.country, databaseData: getCountryLabelByValue(filterdAccounts[0].country), reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; continue; } // 項目チェック(company_name) if (accountFromFile.company_name !== filterdAccounts[0].company_name) { const VerificationResultDetailsOne: VerificationResultDetails = { input: "Account_transition", inputRow: accountFromFile.row, diffTargetTable: "accounts", columnName: "company_name", fileData: accountFromFile.company_name, databaseData: filterdAccounts[0].company_name, reason: "内容不一致", }; VerificationResultDetails.push(VerificationResultDetailsOne); isNoError = false; continue; } } return isNoError; }