diff --git a/data_migration_tools/server/package-lock.json b/data_migration_tools/server/package-lock.json index c1c2143..1a8aff6 100644 --- a/data_migration_tools/server/package-lock.json +++ b/data_migration_tools/server/package-lock.json @@ -26,6 +26,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", + "csv": "^6.3.6", "multer": "^1.4.5-lts.1", "mysql2": "^2.3.3", "reflect-metadata": "^0.1.13", @@ -4049,6 +4050,35 @@ "node": ">= 8" } }, + "node_modules/csv": { + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.3.6.tgz", + "integrity": "sha512-jsEsX2HhGp7xiwrJu5srQavKsh+HUJcCi78Ar3m4jlmFKRoTkkMy7ZZPP+LnQChmaztW+uj44oyfMb59daAs/Q==", + "dependencies": { + "csv-generate": "^4.3.1", + "csv-parse": "^5.5.3", + "csv-stringify": "^6.4.5", + "stream-transform": "^3.3.0" + }, + "engines": { + "node": ">= 0.1.90" + } + }, + "node_modules/csv-generate": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.3.1.tgz", + "integrity": "sha512-7YeeJq+44/I/O5N2sr2qBMcHZXhpfe38eh7DOFxyMtYO+Pir7kIfgFkW5MPksqKqqR6+/wX7UGoZm1Ot11151w==" + }, + "node_modules/csv-parse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.3.tgz", + "integrity": "sha512-v0KW6C0qlZzoGjk6u5tLmVfyZxNgPGXZsWTXshpAgKVGmGXzaVWGdlCFxNx5iuzcXT/oJN1HHM9DZKwtAtYa+A==" + }, + "node_modules/csv-stringify": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.4.5.tgz", + "integrity": "sha512-SPu1Vnh8U5EnzpNOi1NDBL5jU5Rx7DVHr15DNg9LXDTAbQlAVAmEbVt16wZvEW9Fu9Qt4Ji8kmeCJ2B1+4rFTQ==" + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -8654,6 +8684,11 @@ "npm": ">=6" } }, + "node_modules/stream-transform": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.3.0.tgz", + "integrity": "sha512-pG1NeDdmErNYKtvTpFayrEueAmL0xVU5wd22V7InGnatl4Ocq3HY7dcXIKj629kXvYQvglCC7CeDIGAlx1RNGA==" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -13027,6 +13062,32 @@ "which": "^2.0.1" } }, + "csv": { + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.3.6.tgz", + "integrity": "sha512-jsEsX2HhGp7xiwrJu5srQavKsh+HUJcCi78Ar3m4jlmFKRoTkkMy7ZZPP+LnQChmaztW+uj44oyfMb59daAs/Q==", + "requires": { + "csv-generate": "^4.3.1", + "csv-parse": "^5.5.3", + "csv-stringify": "^6.4.5", + "stream-transform": "^3.3.0" + } + }, + "csv-generate": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.3.1.tgz", + "integrity": "sha512-7YeeJq+44/I/O5N2sr2qBMcHZXhpfe38eh7DOFxyMtYO+Pir7kIfgFkW5MPksqKqqR6+/wX7UGoZm1Ot11151w==" + }, + "csv-parse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.3.tgz", + "integrity": "sha512-v0KW6C0qlZzoGjk6u5tLmVfyZxNgPGXZsWTXshpAgKVGmGXzaVWGdlCFxNx5iuzcXT/oJN1HHM9DZKwtAtYa+A==" + }, + "csv-stringify": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.4.5.tgz", + "integrity": "sha512-SPu1Vnh8U5EnzpNOi1NDBL5jU5Rx7DVHr15DNg9LXDTAbQlAVAmEbVt16wZvEW9Fu9Qt4Ji8kmeCJ2B1+4rFTQ==" + }, "date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -16557,6 +16618,11 @@ "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==" }, + "stream-transform": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.3.0.tgz", + "integrity": "sha512-pG1NeDdmErNYKtvTpFayrEueAmL0xVU5wd22V7InGnatl4Ocq3HY7dcXIKj629kXvYQvglCC7CeDIGAlx1RNGA==" + }, "streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", diff --git a/data_migration_tools/server/package.json b/data_migration_tools/server/package.json index a2299cb..a0502b7 100644 --- a/data_migration_tools/server/package.json +++ b/data_migration_tools/server/package.json @@ -45,7 +45,8 @@ "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", "swagger-cli": "^4.0.4", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "csv": "^6.3.6" }, "devDependencies": { "@types/express": "^4.17.17", diff --git a/data_migration_tools/server/src/app.module.ts b/data_migration_tools/server/src/app.module.ts index 6935bba..bcf32fe 100644 --- a/data_migration_tools/server/src/app.module.ts +++ b/data_migration_tools/server/src/app.module.ts @@ -27,6 +27,10 @@ import { DeleteService } from "./features/delete/delete.service"; import { TransferModule } from "./features/transfer/transfer.module"; import { TransferController } from "./features/transfer/transfer.controller"; import { TransferService } from "./features/transfer/transfer.service"; +import { VerificationController } from "./features/verification/verification.controller"; +import { VerificationService } from "./features/verification/verification.service"; +import { VerificationModule } from "./features/verification/verification.module"; + @Module({ imports: [ ServeStaticModule.forRoot({ @@ -41,6 +45,7 @@ import { TransferService } from "./features/transfer/transfer.service"; UsersModule, TransferModule, RegisterModule, + VerificationModule, AccountsRepositoryModule, UsersRepositoryModule, SortCriteriaRepositoryModule, @@ -70,6 +75,7 @@ import { TransferService } from "./features/transfer/transfer.service"; UsersController, DeleteController, TransferController, + VerificationController, ], providers: [ RegisterService, @@ -77,6 +83,7 @@ import { TransferService } from "./features/transfer/transfer.service"; UsersService, DeleteService, TransferService, + VerificationService, ], }) export class AppModule { diff --git a/data_migration_tools/server/src/common/password/password.ts b/data_migration_tools/server/src/common/password/password.ts index 6fbe071..15d52ce 100644 --- a/data_migration_tools/server/src/common/password/password.ts +++ b/data_migration_tools/server/src/common/password/password.ts @@ -18,7 +18,8 @@ export const makePassword = (): string => { let autoGeneratedPassword: string = ""; while (!valid) { - // パスワードをランダムに決定 + autoGeneratedPassword = ""; + // パスワードをランダムに決定+ while (autoGeneratedPassword.length < passLength) { // 上で決定したcharsの中からランダムに1文字ずつ追加 const index = Math.floor(Math.random() * chars.length); @@ -30,6 +31,11 @@ export const makePassword = (): string => { valid = autoGeneratedPassword.length == passLength && charaTypePattern.test(autoGeneratedPassword); + if (!valid) { + // autoGeneratedPasswordをログに出す + console.log("Password is not valid"); + console.log(autoGeneratedPassword); + } } return autoGeneratedPassword; }; diff --git a/data_migration_tools/server/src/common/types/types.ts b/data_migration_tools/server/src/common/types/types.ts index de92211..529a4ad 100644 --- a/data_migration_tools/server/src/common/types/types.ts +++ b/data_migration_tools/server/src/common/types/types.ts @@ -8,8 +8,8 @@ export class csvInputFile { last_name: string; country: string; state: string; - start_date: Date; - expired_date: Date; + start_date: string; + expired_date: string; user_email: string; author_id: string; recording_mode: string; @@ -34,7 +34,12 @@ export class csvInputFile { wt19: string; wt20: string; } -export class AccountsOutputFileStep1 { + +export class csvInputFileWithRow extends csvInputFile { + row: number; +} + +export class AccountsFileType { accountId: number; type: string; companyName: string; @@ -43,9 +48,11 @@ export class AccountsOutputFileStep1 { adminName: string; adminMail: string; userId: number; + role: string; + authorId: string; } -export class AccountsOutputFile { +export class AccountsFile { accountId: number; type: number; companyName: string; @@ -54,18 +61,11 @@ export class AccountsOutputFile { adminName: string; adminMail: string; userId: number; + role: string; + authorId: string; } -export class AccountsInputFile { - accountId: number; - type: number; - companyName: string; - country: string; - dealerAccountId?: number; - adminName: string; - adminMail: string; - userId: number; -} -export class UsersOutputFile { + +export class UsersFile { accountId: number; userId: number; name: string; @@ -74,23 +74,7 @@ export class UsersOutputFile { email: string; } -export class UsersInputFile { - accountId: number; - userId: number; - name: string; - role: string; - authorId: string; - email: string; -} - -export class LicensesOutputFile { - expiry_date: string; - account_id: number; - type: string; - status: string; - allocated_user_id?: number; -} -export class LicensesInputFile { +export class LicensesFile { expiry_date: string; account_id: number; type: string; @@ -98,16 +82,12 @@ export class LicensesInputFile { allocated_user_id?: number; } -export class WorktypesOutputFile { - account_id: number; - custom_worktype_id: string; -} -export class WorktypesInputFile { +export class WorktypesFile { account_id: number; custom_worktype_id: string; } -export class CardLicensesInputFile { +export class CardLicensesFile { license_id: number; issue_id: number; card_license_key: string; @@ -118,10 +98,26 @@ export class CardLicensesInputFile { updated_by?: string; } -export function isAccountsInputFileArray(obj: any): obj is AccountsInputFile[] { - return Array.isArray(obj) && obj.every((item) => isAccountsInputFile(item)); +export class AccountsMappingFile { + accountIdText: string; + accountIdNumber: number; } -export function isAccountsInputFile(obj: any): obj is AccountsInputFile { + +export class VerificationResultDetails { + input: string; + inputRow: number; + diffTargetTable: string; + columnName: string; + fileData: string; + databaseData: string; + reason: string; +} + + +export function isAccountsFileArray(obj: any): obj is AccountsFile[] { + return Array.isArray(obj) && obj.every((item) => isAccountsFile(item)); +} +export function isAccountsFile(obj: any): obj is AccountsFile { return ( typeof obj === "object" && obj !== null && @@ -141,14 +137,20 @@ export function isAccountsInputFile(obj: any): obj is AccountsInputFile { "adminMail" in obj && typeof obj.adminMail === "string" && "userId" in obj && - typeof obj.userId === "number" + typeof obj.userId === "number" && + ("role" in obj + ? obj.role === null || typeof obj.role === "string" + : true) && + ("authorId" in obj + ? obj.authorId === null || typeof obj.authorId === "string" + : true) ); } -export function isUsersInputFileArray(obj: any): obj is UsersInputFile[] { - return Array.isArray(obj) && obj.every((item) => isUsersInputFile(item)); +export function isUsersFileArray(obj: any): obj is UsersFile[] { + return Array.isArray(obj) && obj.every((item) => isUsersFile(item)); } -export function isUsersInputFile(obj: any): obj is UsersInputFile { +export function isUsersFile(obj: any): obj is UsersFile { return ( typeof obj === "object" && obj !== null && @@ -167,10 +169,10 @@ export function isUsersInputFile(obj: any): obj is UsersInputFile { ); } -export function isLicensesInputFileArray(obj: any): obj is LicensesInputFile[] { - return Array.isArray(obj) && obj.every((item) => isLicensesInputFile(item)); +export function isLicensesFileArray(obj: any): obj is LicensesFile[] { + return Array.isArray(obj) && obj.every((item) => isLicensesFile(item)); } -export function isLicensesInputFile(obj: any): obj is LicensesInputFile { +export function isLicensesFile(obj: any): obj is LicensesFile { return ( typeof obj === "object" && obj !== null && @@ -187,12 +189,10 @@ export function isLicensesInputFile(obj: any): obj is LicensesInputFile { ); } -export function isWorktypesInputFileArray( - obj: any -): obj is WorktypesInputFile[] { - return Array.isArray(obj) && obj.every((item) => isWorktypesInputFile(item)); +export function isWorktypesFileArray(obj: any): obj is WorktypesFile[] { + return Array.isArray(obj) && obj.every((item) => isWorktypesFile(item)); } -export function isWorktypesInputFile(obj: any): obj is WorktypesInputFile { +export function isWorktypesFile(obj: any): obj is WorktypesFile { return ( typeof obj === "object" && obj !== null && @@ -203,16 +203,10 @@ export function isWorktypesInputFile(obj: any): obj is WorktypesInputFile { ); } -export function isCardLicensesInputFileArray( - obj: any -): obj is CardLicensesInputFile[] { - return ( - Array.isArray(obj) && obj.every((item) => isCardLicensesInputFile(item)) - ); +export function isCardLicensesFileArray(obj: any): obj is CardLicensesFile[] { + return Array.isArray(obj) && obj.every((item) => isCardLicensesFile(item)); } -export function isCardLicensesInputFile( - obj: any -): obj is CardLicensesInputFile { +export function isCardLicensesFile(obj: any): obj is CardLicensesFile { return ( typeof obj === "object" && obj !== null && @@ -229,3 +223,65 @@ export function isCardLicensesInputFile( (obj.updated_by === null || typeof obj.updated_by === "string") ); } + +export function isAccountsMappingFileArray( + obj: any +): obj is AccountsMappingFile[] { + return Array.isArray(obj) && obj.every((item) => isAccountsMappingFile(item)); +} +export function isAccountsMappingFile(obj: any): obj is AccountsMappingFile { + return ( + typeof obj === "object" && + obj !== null && + "accountIdText" in obj && + "accountIdNumber" in obj && + typeof obj.accountIdText === "string" && + typeof obj.accountIdNumber === "number" + ); +} + +export function isCsvInputFileForValidateArray(obj: any): obj is csvInputFile[] { + return ( + Array.isArray(obj) && obj.every((item) => isCsvInputFileForValidate(item)) + ); +} + +export function isCsvInputFileForValidate(obj: any): obj is csvInputFile { + return ( + typeof obj === "object" && + "type" in obj && + "account_id" in obj && + "parent_id" in obj && + "email" in obj && + "company_name" in obj && + "first_name" in obj && + "last_name" in obj && + "country" in obj && + "state" in obj && + "start_date" in obj && + "expired_date" in obj && + "user_email" in obj && + "author_id" in obj && + "recording_mode" in obj && + "wt1" in obj && + "wt2" in obj && + "wt3" in obj && + "wt4" in obj && + "wt5" in obj && + "wt6" in obj && + "wt7" in obj && + "wt8" in obj && + "wt9" in obj && + "wt10" in obj && + "wt11" in obj && + "wt12" in obj && + "wt13" in obj && + "wt14" in obj && + "wt15" in obj && + "wt16" in obj && + "wt17" in obj && + "wt18" in obj && + "wt19" in obj && + "wt20" in obj + ); +} diff --git a/data_migration_tools/server/src/constants/index.ts b/data_migration_tools/server/src/constants/index.ts index 71bd022..74e6e3a 100644 --- a/data_migration_tools/server/src/constants/index.ts +++ b/data_migration_tools/server/src/constants/index.ts @@ -351,7 +351,7 @@ export const COUNTRY_LIST = [ { value: "BG", label: "Bulgaria" }, { value: "HR", label: "Croatia" }, { value: "CY", label: "Cyprus" }, - { value: "CZ", label: "Czech Republic" }, + { value: "CZ", label: "Czech" }, { value: "DK", label: "Denmark" }, { value: "EE", label: "Estonia" }, { value: "FI", label: "Finland" }, diff --git a/data_migration_tools/server/src/features/accounts/accounts.service.ts b/data_migration_tools/server/src/features/accounts/accounts.service.ts index 4a7dd65..fdfb458 100644 --- a/data_migration_tools/server/src/features/accounts/accounts.service.ts +++ b/data_migration_tools/server/src/features/accounts/accounts.service.ts @@ -37,6 +37,7 @@ export class AccountsService { password: string, username: string, role: string, + authorId: string, acceptedEulaVersion: string, acceptedPrivacyNoticeVersion: string, acceptedDpaVersion: string, @@ -78,6 +79,7 @@ export class AccountsService { HttpStatus.INTERNAL_SERVER_ERROR ); } + this.logger.log("idpにユーザーを作成成功"); // メールアドレス重複エラー if (isConflictError(externalUser)) { @@ -89,6 +91,7 @@ export class AccountsService { HttpStatus.BAD_REQUEST ); } + this.logger.log("メールアドレスは重複していません"); let account: Account; let user: User; @@ -103,6 +106,7 @@ export class AccountsService { type, externalUser.sub, role, + authorId, accountId, userId, acceptedEulaVersion, @@ -136,6 +140,7 @@ export class AccountsService { account.id, country ); + this.logger.log("コンテナー作成成功"); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); this.logger.error( diff --git a/data_migration_tools/server/src/features/register/register.controller.ts b/data_migration_tools/server/src/features/register/register.controller.ts index c55d548..d9b968f 100644 --- a/data_migration_tools/server/src/features/register/register.controller.ts +++ b/data_migration_tools/server/src/features/register/register.controller.ts @@ -17,11 +17,11 @@ import { AccountsService } from "../accounts/accounts.service"; import { UsersService } from "../users/users.service"; import { makeContext } from "../../common/log"; import { - isAccountsInputFileArray, - isUsersInputFileArray, - isLicensesInputFileArray, - isWorktypesInputFileArray, - isCardLicensesInputFileArray, + isAccountsFileArray, + isUsersFileArray, + isLicensesFileArray, + isWorktypesFileArray, + isCardLicensesFileArray, } from "../../common/types/types"; import { makePassword } from "../../common/password/password"; import { @@ -73,13 +73,24 @@ export class RegisterController { const cardLicensesFileFullPath = inputFilePath + "cardLicenses.json"; // ファイル存在チェックと読み込み - if ( - !fs.existsSync(accouncsFileFullPath) || - !fs.existsSync(usersFileFullPath) || - !fs.existsSync(licensesFileFullPath) || - !fs.existsSync(worktypesFileFullPath) || - !fs.existsSync(cardLicensesFileFullPath) - ) { + // どのファイルがないのかわからないのでそれぞれに存在しない場合はエラーを出す + if (!fs.existsSync(accouncsFileFullPath)) { + this.logger.error(`file not exists from ${inputFilePath}`); + throw new Error(`file not exists from ${inputFilePath}`); + } + if (!fs.existsSync(usersFileFullPath)) { + this.logger.error(`file not exists from ${inputFilePath}`); + throw new Error(`file not exists from ${inputFilePath}`); + } + if (!fs.existsSync(licensesFileFullPath)) { + this.logger.error(`file not exists from ${inputFilePath}`); + throw new Error(`file not exists from ${inputFilePath}`); + } + if (!fs.existsSync(worktypesFileFullPath)) { + this.logger.error(`file not exists from ${inputFilePath}`); + throw new Error(`file not exists from ${inputFilePath}`); + } + if (!fs.existsSync(cardLicensesFileFullPath)) { this.logger.error(`file not exists from ${inputFilePath}`); throw new Error(`file not exists from ${inputFilePath}`); } @@ -90,34 +101,53 @@ export class RegisterController { ); // 型ガード(account) - if (!isAccountsInputFileArray(accountsObject)) { - throw new Error("input file is not accountsInputFiles"); + if (!isAccountsFileArray(accountsObject)) { + throw new Error("input file is not AccountsFiles"); } - for (const accountsInputFile of accountsObject) { + for (const AccountsFile of accountsObject) { + this.logger.log("ランダムパスワード生成開始"); // ランダムなパスワードを生成する const ramdomPassword = makePassword(); + this.logger.log("ランダムパスワード生成完了"); + // roleの設定 + // roleの値がnullなら"none"、null以外ならroleの値、 + // また、roleの値が"author"なら"author"を設定 + let role: string; + let authorId: string; + if (AccountsFile.role === null) { + role = USER_ROLES.NONE; + authorId = null; + } else if (AccountsFile.role === USER_ROLES.AUTHOR) { + role = USER_ROLES.AUTHOR; + authorId = AccountsFile.authorId; + } else { + // ありえないが、roleの値が"none"または"author"の文字列以外の場合はエラーを返す + throw new Error("Invalid role value"); + } + this.logger.log("account生成開始"); await this.accountsService.createAccount( context, - accountsInputFile.companyName, - accountsInputFile.country, - accountsInputFile.dealerAccountId, - accountsInputFile.adminMail, + AccountsFile.companyName, + AccountsFile.country, + AccountsFile.dealerAccountId, + AccountsFile.adminMail, ramdomPassword, - accountsInputFile.adminName, - "none", + AccountsFile.adminName, + role, + authorId, null, null, null, - accountsInputFile.type, - accountsInputFile.accountId, - accountsInputFile.userId + AccountsFile.type, + AccountsFile.accountId, + AccountsFile.userId ); // ratelimit対応のためsleepを行う await sleep(MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC); } - // const accountsInputFiles = accountsObject as AccountsInputFile[]; + // const AccountsFiles = accountsObject as AccountsFile[]; // ユーザの登録用ファイル読み込み const usersObject = JSON.parse( @@ -125,24 +155,24 @@ export class RegisterController { ); // 型ガード(user) - if (!isUsersInputFileArray(usersObject)) { - throw new Error("input file is not usersInputFiles"); + if (!isUsersFileArray(usersObject)) { + throw new Error("input file is not UsersFiles"); } - for (const usersInputFile of usersObject) { - this.logger.log(usersInputFile.name); + for (const UsersFile of usersObject) { + this.logger.log(UsersFile.name); await this.usersService.createUser( context, - usersInputFile.name, - usersInputFile.role === USER_ROLES.AUTHOR + UsersFile.name, + UsersFile.role === USER_ROLES.AUTHOR ? USER_ROLES.AUTHOR : USER_ROLES.NONE, - usersInputFile.email, + UsersFile.email, true, true, - usersInputFile.accountId, - usersInputFile.userId, - usersInputFile.authorId, + UsersFile.accountId, + UsersFile.userId, + UsersFile.authorId, false, null, true @@ -157,8 +187,8 @@ export class RegisterController { ); // 型ガード(license) - if (!isLicensesInputFileArray(licensesObject)) { - throw new Error("input file is not licensesInputFiles"); + if (!isLicensesFileArray(licensesObject)) { + throw new Error("input file is not LicensesFiles"); } // ワークタイプの登録用ファイル読み込み @@ -167,8 +197,8 @@ export class RegisterController { ); // 型ガード(Worktypes) - if (!isWorktypesInputFileArray(worktypesObject)) { - throw new Error("input file is not WorktypesInputFiles"); + if (!isWorktypesFileArray(worktypesObject)) { + throw new Error("input file is not WorktypesFiles"); } // カードライセンスの登録用ファイル読み込み @@ -177,8 +207,8 @@ export class RegisterController { ); // 型ガード(cardLicenses) - if (!isCardLicensesInputFileArray(cardLicensesObject)) { - throw new Error("input file is not cardLicensesInputFiles"); + if (!isCardLicensesFileArray(cardLicensesObject)) { + throw new Error("input file is not cardLicensesFiles"); } // ライセンス・ワークタイプ・カードライセンスの登録 diff --git a/data_migration_tools/server/src/features/register/register.service.ts b/data_migration_tools/server/src/features/register/register.service.ts index 6e42dc1..9af654f 100644 --- a/data_migration_tools/server/src/features/register/register.service.ts +++ b/data_migration_tools/server/src/features/register/register.service.ts @@ -1,9 +1,9 @@ import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; import { Context } from "../../common/log"; import { - LicensesInputFile, - WorktypesInputFile, - CardLicensesInputFile, + LicensesFile, + WorktypesFile, + CardLicensesFile, } from "../../common/types/types"; import { LicensesRepositoryService } from "../../repositories/licenses/licenses.repository.service"; import { WorktypesRepositoryService } from "../../repositories/worktypes/worktypes.repository.service"; @@ -22,9 +22,9 @@ export class RegisterService { */ async registLicenseAndWorktypeData( context: Context, - licensesInputFiles: LicensesInputFile[], - worktypesInputFiles: WorktypesInputFile[], - cardlicensesInputFiles: CardLicensesInputFile[] + LicensesFiles: LicensesFile[], + WorktypesFiles: WorktypesFile[], + cardLicensesFiles: CardLicensesFile[] ): Promise { // パラメータ内容が長大なのでログには出さない this.logger.log( @@ -35,20 +35,17 @@ export class RegisterService { try { this.logger.log("Licenses register start"); - await this.licensesRepository.insertLicenses(context, licensesInputFiles); + await this.licensesRepository.insertLicenses(context, LicensesFiles); this.logger.log("Licenses register end"); this.logger.log("Worktypes register start"); - await this.worktypesRepository.createWorktype( - context, - worktypesInputFiles - ); + await this.worktypesRepository.createWorktype(context, WorktypesFiles); this.logger.log("Worktypes register end"); this.logger.log("CardLicenses register start"); await this.licensesRepository.insertCardLicenses( context, - cardlicensesInputFiles + cardLicensesFiles ); this.logger.log("CardLicenses register end"); } catch (e) { diff --git a/data_migration_tools/server/src/features/transfer/transfer.controller.ts b/data_migration_tools/server/src/features/transfer/transfer.controller.ts index 8725a4c..c3c2dce 100644 --- a/data_migration_tools/server/src/features/transfer/transfer.controller.ts +++ b/data_migration_tools/server/src/features/transfer/transfer.controller.ts @@ -13,18 +13,9 @@ import { Request } from "express"; import { transferRequest, transferResponse } from "./types/types"; import { TransferService } from "./transfer.service"; import { makeContext } from "../../common/log"; -import { csvInputFile } from "../../common/types/types"; +import { csvInputFile, AccountsMappingFile } from "../../common/types/types"; import { makeErrorResponse } from "src/common/errors/makeErrorResponse"; -import { - COUNTRY_LIST, - MIGRATION_TYPE, - TIERS, - WORKTYPE_MAX_COUNT, - RECORDING_MODE, - LICENSE_ALLOCATED_STATUS, - USER_ROLES, - AUTO_INCREMENT_START, -} from "../../constants"; +import { AUTO_INCREMENT_START } from "../../constants"; @ApiTags("transfer") @Controller("transfer") export class TransferController { @@ -57,16 +48,16 @@ export class TransferController { ); try { // 読み込みファイルのフルパス - const csvFileFullPath = inputFilePath + ".csv"; + const accouncsFileFullPath = inputFilePath + "Account_transition.csv"; // ファイル存在チェックと読み込み - if (!fs.existsSync(csvFileFullPath)) { + if (!fs.existsSync(accouncsFileFullPath)) { this.logger.error(`file not exists from ${inputFilePath}`); throw new Error(`file not exists from ${inputFilePath}`); } // CSVファイルを全行読み込む - const inputFile = fs.readFileSync(csvFileFullPath, "utf-8"); + const inputFile = fs.readFileSync(accouncsFileFullPath, "utf-8"); // レコードごとに分割 const csvInputFileLines = inputFile.split("\n"); @@ -77,49 +68,78 @@ export class TransferController { // 項目ごとに切り分ける let csvInputFile: csvInputFile[] = []; csvInputFileLines.forEach((line) => { + // 項目にカンマが入っている場合を考慮して、ダブルクォーテーションで囲まれた部分を一つの項目として扱う + const regExp = /"[^"]*"/g; + const matchList = line.match(regExp); + if (matchList) { + matchList.forEach((match) => { + // カンマを\に変換 + const replaced = match.replace(/,/g, "\\"); + line = line.replace(match, replaced); + }); + } const data = line.split(","); - // ダブルクォーテーションは削除 + // ダブルクォーテーションを削除 data.forEach((value, index) => { data[index] = value.replace(/"/g, ""); }); - csvInputFile.push({ - type: data[0], - account_id: data[1], - parent_id: data[2], - email: data[3], - company_name: data[4], - first_name: data[5], - last_name: data[6], - country: data[7], - state: data[8], - start_date: new Date(data[9]), - expired_date: new Date(data[10]), - user_email: data[11], - author_id: data[12], - recording_mode: data[13], - wt1: data[14], - wt2: data[15], - wt3: data[16], - wt4: data[17], - wt5: data[18], - wt6: data[19], - wt7: data[20], - wt8: data[21], - wt9: data[22], - wt10: data[23], - wt11: data[24], - wt12: data[25], - wt13: data[26], - wt14: data[27], - wt15: data[28], - wt16: data[29], - wt17: data[30], - wt18: data[31], - wt19: data[32], - wt20: data[33], - }); + // "\r"を削除 + data[data.length - 1] = data[data.length - 1].replace(/\r/g, ""); + // dataの要素数が34(csvInputFileの要素数)より多い場合、フォーマット不一致エラー(移行元はworktypeの数が20より多く設定できるので理論上は存在する) + // worktypeの数の確認を促すエラーを出す + if (data.length > 34) { + this.logger.error( + `[${context.getTrackingId()}] format error.please check the number of elements in worktype. data=${data}` + ); + throw new HttpException( + makeErrorResponse("E009999"), + HttpStatus.BAD_REQUEST + ); + } + // data[1]がundefinedの場合、配列には格納しない + if (data[1] !== undefined) { + // バックスラッシュをカンマに戻す + data.forEach((value, index) => { + data[index] = value.replace(/\\/g, ","); + }); + csvInputFile.push({ + type: data[0], + account_id: data[1], + parent_id: data[2], + email: data[3], + company_name: data[4], + first_name: data[5], + last_name: data[6], + country: data[7], + state: data[8], + start_date: data[9], + expired_date: data[10], + user_email: data[11], + author_id: data[12], + recording_mode: data[13], + wt1: data[14], + wt2: data[15], + wt3: data[16], + wt4: data[17], + wt5: data[18], + wt6: data[19], + wt7: data[20], + wt8: data[21], + wt9: data[22], + wt10: data[23], + wt11: data[24], + wt12: data[25], + wt13: data[26], + wt14: data[27], + wt15: data[28], + wt16: data[29], + wt17: data[30], + wt18: data[31], + wt19: data[32], + wt20: data[33], + }); + } }); - // 各データのバリデーションチェック await this.transferService.validateInputData(context, csvInputFile); @@ -131,36 +151,63 @@ export class TransferController { accountIdListArray.forEach((accountId, index) => { accountIdMap.set(accountId, index + AUTO_INCREMENT_START); }); + + // アカウントID numberとstring対応表の出力 + const accountsMappingFiles: AccountsMappingFile[] = []; + accountIdMap.forEach((value, key) => { + const accountsMappingFile = new AccountsMappingFile(); + accountsMappingFile.accountIdNumber = value; + accountsMappingFile.accountIdText = key; + accountsMappingFiles.push(accountsMappingFile); + }); + + fs.writeFileSync( + `${inputFilePath}account_map.json`, + JSON.stringify(accountsMappingFiles) + ); + // CSVファイルの変換 - const transferResponse = await this.transferService.registInputData( + const transferResponseCsv = await this.transferService.transferInputData( context, csvInputFile, accountIdMap ); // countryを除いた階層の再配置 - const accountsOutputFileStep1Lines = - transferResponse.accountsOutputFileStep1Lines; - const accountsOutputFile = await this.transferService.relocateHierarchy( + const AccountsFileTypeLines = transferResponseCsv.accountsFileTypeLines; + const AccountsFile = await this.transferService.relocateHierarchy( context, - accountsOutputFileStep1Lines + AccountsFileTypeLines ); + const UsersFile = transferResponseCsv.usersFileLines; + const LicensesFile = transferResponseCsv.licensesFileLines; // メールアドレスの重複を削除 - // デモライセンスの削除 - // いったんこのままコミットしてテストを実施する + const resultDuplicateEmail = + await this.transferService.removeDuplicateEmail( + context, + AccountsFile, + UsersFile, + LicensesFile + ); - // transferResponseを4つのJSONファイルの出力する(出力先はinputと同じにする) + // AuthorIDが重複している場合通番を付与する + const transferDuplicateAuthorResultUsers = + await this.transferService.transferDuplicateAuthor( + context, + resultDuplicateEmail.accountsFileLines, + resultDuplicateEmail.usersFileLines + ); + + // transferResponseCsvを4つのJSONファイルの出力する(出力先はinputと同じにする) const outputFilePath = body.inputFilePath; - const usersOutputFile = transferResponse.usersOutputFileLines; - const licensesOutputFile = transferResponse.licensesOutputFileLines; - const worktypesOutputFile = transferResponse.worktypesOutputFileLines; + const WorktypesFile = transferResponseCsv.worktypesFileLines; this.transferService.outputJsonFile( context, outputFilePath, - accountsOutputFile, - usersOutputFile, - licensesOutputFile, - worktypesOutputFile + resultDuplicateEmail.accountsFileLines, + transferDuplicateAuthorResultUsers, + resultDuplicateEmail.licensesFileLines, + WorktypesFile ); return {}; } catch (e) { diff --git a/data_migration_tools/server/src/features/transfer/transfer.service.ts b/data_migration_tools/server/src/features/transfer/transfer.service.ts index 31b75ed..8168c04 100644 --- a/data_migration_tools/server/src/features/transfer/transfer.service.ts +++ b/data_migration_tools/server/src/features/transfer/transfer.service.ts @@ -1,12 +1,12 @@ import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; import { Context } from "../../common/log"; import { - AccountsOutputFileStep1, - UsersOutputFile, - LicensesOutputFile, - WorktypesOutputFile, + AccountsFileType, + UsersFile, + LicensesFile, + WorktypesFile, csvInputFile, - AccountsOutputFile, + AccountsFile, } from "../../common/types/types"; import { COUNTRY_LIST, @@ -18,8 +18,12 @@ import { USER_ROLES, SWITCH_FROM_TYPE, } from "src/constants"; -import { registInputDataResponse } from "./types/types"; +import { + registInputDataResponse, + removeDuplicateEmailResponse, +} from "./types/types"; import fs from "fs"; +import { makeErrorResponse } from "src/common/error/makeErrorResponse"; @Injectable() export class TransferService { @@ -27,40 +31,51 @@ export class TransferService { private readonly logger = new Logger(TransferService.name); /** - * Regist Data + * Transfer Input Data * @param OutputFilePath: string * @param csvInputFile: csvInputFile[] */ - async registInputData( + async transferInputData( context: Context, csvInputFile: csvInputFile[], accountIdMap: Map ): Promise { // パラメータ内容が長大なのでログには出さない this.logger.log( - `[IN] [${context.getTrackingId()}] ${this.registInputData.name}` + `[IN] [${context.getTrackingId()}] ${this.transferInputData.name}` ); try { - let accountsOutputFileStep1Lines: AccountsOutputFileStep1[] = []; - let usersOutputFileLines: UsersOutputFile[] = []; - let licensesOutputFileLines: LicensesOutputFile[] = []; - let worktypesOutputFileLines: WorktypesOutputFile[] = []; - + 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) { - // userIdのインクリメント - userIdIndex = userIdIndex + 1; // line.countryの値を読み込みCOUNTRY_LISTのlabelからvalueに変換する const country = COUNTRY_LIST.find( (country) => country.label === line.country )?.value; // adminNameの変換(last_name + " "+ first_name) - const adminName = `${line.last_name} ${line.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(); @@ -71,8 +86,18 @@ export class TransferService { if (line.parent_id) { parentAccountId = accountIdMap.get(line.parent_id); } - // AccountsOutputFile配列にPush - accountsOutputFileStep1Lines.push({ + // 万が一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, @@ -82,150 +107,180 @@ export class TransferService { adminName: adminName, adminMail: line.email, userId: userIdIndex, + role: null, + authorId: null, }); } else { - // typeが"USER"の場合、ユーザデータの作成を行う - // userIdのインクリメント - userIdIndex = userIdIndex + 1; - // nameの変換 - // もしline.last_nameとline.first_nameが存在しない場合、line.emailをnameにする - // 存在する場合は、last_name + " " + first_name - let name = line.email; - if (line.last_name && line.first_name) { - name = `${line.last_name} ${line.first_name}`; - } - // roleの変換 - // authorIdが設定されてる場合はauthor、されていない場合は移行しないので次の行に進む - if (line.author_id) { - usersOutputFileLines.push({ - accountId: accountIdMap.get(line.account_id), - userId: userIdIndex, - name: name, - role: USER_ROLES.AUTHOR, - authorId: line.author_id, - email: line.user_email, - }); - } else { - return; - } - // ライセンスのデータの作成を行う - // authorIdが設定されてる場合、statusは"allocated"、allocated_user_idは対象のユーザID - // されていない場合、statusは"reusable"、allocated_user_idはnull - licensesOutputFileLines.push({ - expiry_date: line.expired_date.toISOString(), - account_id: accountIdMap.get(line.account_id), - type: SWITCH_FROM_TYPE.NONE, - status: line.author_id - ? LICENSE_ALLOCATED_STATUS.ALLOCATED - : LICENSE_ALLOCATED_STATUS.REUSABLE, - allocated_user_id: line.author_id ? userIdIndex : null, - }); - // WorktypesOutputFileの作成 - // wt1~wt20まで読み込み、account単位で作成する - // 作成したWorktypesOutputFileを配列にPush - for (let i = 1; i <= WORKTYPE_MAX_COUNT; i++) { - const wt = `wt${i}`; - if (line[wt]) { - // 既に存在する場合は、作成しない - if ( - worktypesOutputFileLines.find( - (worktype) => - worktype.account_id === accountIdMap.get(line.account_id) && - worktype.custom_worktype_id === line[wt].toString() - ) - ) { - continue; + // 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 { - accountsOutputFileStep1Lines, - usersOutputFileLines, - licensesOutputFileLines, - worktypesOutputFileLines, + 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.registInputData.name}` + `[OUT] [${context.getTrackingId()}] ${this.transferInputData.name}` ); } } /** * 階層の付け替えを行う - * @param accountsOutputFileStep1: AccountsOutputFileStep1[] - * @returns AccountsOutputFile[] + * @param accountsFileType: AccountsFileType[] + * @returns AccountsFile[] */ async relocateHierarchy( context: Context, - accountsOutputFileStep1: AccountsOutputFileStep1[] - ): Promise { + accountsFileType: AccountsFileType[] + ): Promise { // パラメータ内容が長大なのでログには出さない this.logger.log( `[IN] [${context.getTrackingId()}] ${this.relocateHierarchy.name}` ); try { - // dealerAccountIdを検索し、typeがCountryの場合 - accountsOutputFileStep1.forEach((account) => { - if (account.type === MIGRATION_TYPE.COUNTRY) { - console.log(account); - // そのacccountIdをdealerAccountIdにもつアカウント(Distributor)を検索する - const distributor = accountsOutputFileStep1.find( - (distributor) => distributor.accountId === account.dealerAccountId - ); - console.log(distributor); - if (distributor) { - // DistributorのdealerAccountIdをBC(Countryの親)に付け替える - distributor.dealerAccountId = account.dealerAccountId; - } - } - }); - // typeがCountryのアカウントを取り除く - accountsOutputFileStep1 = accountsOutputFileStep1.filter( - (account) => account.type !== MIGRATION_TYPE.COUNTRY + const relocatedAccounts: AccountsFile[] = []; + const dealerRecords: Map = new Map(); + + const countryAccounts = accountsFileType.filter( + (item) => item.type === MIGRATION_TYPE.COUNTRY ); - // typeをtierに変換し、AccountsOutputFileに変換する - let accountsOutputFile: AccountsOutputFile[] = []; - accountsOutputFileStep1.forEach((account) => { - let tier = 0; - switch (account.type) { - case MIGRATION_TYPE.ADMINISTRATOR: - tier = TIERS.TIER1; - break; - case MIGRATION_TYPE.BC: - tier = TIERS.TIER2; - break; - case MIGRATION_TYPE.DISTRIBUTOR: - tier = TIERS.TIER3; - break; - case MIGRATION_TYPE.DEALER: - tier = TIERS.TIER4; - break; - case MIGRATION_TYPE.CUSTOMER: - tier = TIERS.TIER5; - break; + 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; + } } - accountsOutputFile.push({ - accountId: account.accountId, - type: tier, - companyName: account.companyName, - country: account.country, - dealerAccountId: account.dealerAccountId, - adminName: account.adminName, - adminMail: account.adminMail, - userId: account.userId, - }); + + 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 accountsOutputFile; + + 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}` @@ -233,21 +288,38 @@ export class TransferService { } } + // メソッド: アカウントタイプを数値に変換するヘルパー関数 + 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 accountsOutputFile: AccountsOutputFile[] - * @param usersOutputFile: UsersOutputFile[] - * @param licensesOutputFile: LicensesOutputFile[] - * @param worktypesOutputFile: WorktypesOutputFile[] + * @param accountsFile: AccountsFile[] + * @param usersFile: UsersFile[] + * @param licensesFile: LicensesFile[] + * @param worktypesFile: WorktypesFile[] */ async outputJsonFile( context: Context, outputFilePath: string, - accountsOutputFile: AccountsOutputFile[], - usersOutputFile: UsersOutputFile[], - licensesOutputFile: LicensesOutputFile[], - worktypesOutputFile: WorktypesOutputFile[] + accountsFile: AccountsFile[], + usersFile: UsersFile[], + licensesFile: LicensesFile[], + worktypesFile: WorktypesFile[] ): Promise { // パラメータ内容が長大なのでログには出さない this.logger.log( @@ -256,29 +328,24 @@ export class TransferService { try { // JSONファイルの出力を行う - // accountsOutputFile配列の出力 - const accountsOutputFileJson = JSON.stringify(accountsOutputFile); - fs.writeFileSync( - `${outputFilePath}_accounts.json`, - accountsOutputFileJson - ); - // usersOutputFile - const usersOutputFileJson = JSON.stringify(usersOutputFile); - fs.writeFileSync(`${outputFilePath}_users.json`, usersOutputFileJson); - // licensesOutputFile - const licensesOutputFileJson = JSON.stringify(licensesOutputFile); - fs.writeFileSync( - `${outputFilePath}_licenses.json`, - licensesOutputFileJson - ); - // worktypesOutputFile - const worktypesOutputFileJson = JSON.stringify(worktypesOutputFile); - fs.writeFileSync( - `${outputFilePath}_worktypes.json`, - worktypesOutputFileJson - ); + // 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}` @@ -300,6 +367,10 @@ export class TransferService { ); try { + // エラー配列を定義 + let errorArray: string[] = []; + // アカウントに対するworktypeのMap配列を作成する + const accountWorktypeMap = new Map(); // csvInputFileのバリデーションチェックを行う csvInputFile.forEach((line, index) => { // typeのバリデーションチェック @@ -317,10 +388,16 @@ export class TransferService { 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)) { - console.log(line.country); throw new HttpException( `country is invalid. index=${index} country=${line.country}`, HttpStatus.BAD_REQUEST @@ -329,7 +406,8 @@ export class TransferService { } // mailのバリデーションチェック // メールアドレスの形式が正しいかどうかのチェック - const mailRegExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + 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( @@ -360,13 +438,260 @@ export class TransferService { ); } } + // 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 === account.adminMail + ); + + 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 === user.email + ); + + 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 === user.email + ); + // 重複がある場合 + 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 + }` + ); + } + } } diff --git a/data_migration_tools/server/src/features/transfer/types/types.ts b/data_migration_tools/server/src/features/transfer/types/types.ts index 63eb3ab..d4dd969 100644 --- a/data_migration_tools/server/src/features/transfer/types/types.ts +++ b/data_migration_tools/server/src/features/transfer/types/types.ts @@ -1,9 +1,10 @@ import { ApiProperty } from "@nestjs/swagger"; import { - AccountsOutputFileStep1, - LicensesOutputFile, - UsersOutputFile, - WorktypesOutputFile, + AccountsFile, + AccountsFileType, + LicensesFile, + UsersFile, + WorktypesFile, } from "src/common/types/types"; export class transferRequest { @@ -15,11 +16,20 @@ export class transferResponse {} export class registInputDataResponse { @ApiProperty() - accountsOutputFileStep1Lines: AccountsOutputFileStep1[]; + accountsFileTypeLines: AccountsFileType[]; @ApiProperty() - usersOutputFileLines: UsersOutputFile[]; + usersFileLines: UsersFile[]; @ApiProperty() - licensesOutputFileLines: LicensesOutputFile[]; + licensesFileLines: LicensesFile[]; @ApiProperty() - worktypesOutputFileLines: WorktypesOutputFile[]; + worktypesFileLines: WorktypesFile[]; +} + +export class removeDuplicateEmailResponse { + @ApiProperty() + accountsFileLines: AccountsFile[]; + @ApiProperty() + usersFileLines: UsersFile[]; + @ApiProperty() + licensesFileLines: LicensesFile[]; } diff --git a/data_migration_tools/server/src/features/users/users.service.ts b/data_migration_tools/server/src/features/users/users.service.ts index cb639cd..8134462 100644 --- a/data_migration_tools/server/src/features/users/users.service.ts +++ b/data_migration_tools/server/src/features/users/users.service.ts @@ -74,6 +74,9 @@ export class UsersService { accountId, authorId ); + this.logger.log( + `[${context.getTrackingId()}] isAuthorIdDuplicated=${isAuthorIdDuplicated}` + ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); throw new HttpException( @@ -88,9 +91,10 @@ export class UsersService { ); } } - + this.logger.log("ランダムパスワード生成開始"); // ランダムなパスワードを生成する const ramdomPassword = makePassword(); + this.logger.log("ランダムパスワード生成完了"); //Azure AD B2Cにユーザーを新規登録する let externalUser: { sub: string } | ConflictError; diff --git a/data_migration_tools/server/src/features/verification/types/types.ts b/data_migration_tools/server/src/features/verification/types/types.ts new file mode 100644 index 0000000..2eda873 --- /dev/null +++ b/data_migration_tools/server/src/features/verification/types/types.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class VerificationRequest { + @ApiProperty() + inputFilePath: string; +} + +export class VerificationResponse {} + diff --git a/data_migration_tools/server/src/features/verification/verification.controller.ts b/data_migration_tools/server/src/features/verification/verification.controller.ts new file mode 100644 index 0000000..31cde89 --- /dev/null +++ b/data_migration_tools/server/src/features/verification/verification.controller.ts @@ -0,0 +1,148 @@ +import { + Body, + Controller, + HttpStatus, + Post, + Req, + Logger, + HttpException, +} from "@nestjs/common"; +import { makeErrorResponse } from "../../common/error/makeErrorResponse"; +import fs from "fs"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { Request } from "express"; +import { VerificationRequest, VerificationResponse } from "./types/types"; +import { VerificationService } from "./verification.service"; +import { makeContext } from "../../common/log"; +import { + csvInputFileWithRow, + isAccountsMappingFileArray, + isCardLicensesFileArray, + isCsvInputFileForValidateArray, +} from "../../common/types/types"; +import * as csv from "csv"; + +@ApiTags("verification") +@Controller("verification") +export class VerificationController { + private readonly logger = new Logger(VerificationController.name); + constructor(private readonly verificationService: VerificationService) {} + + @Post() + @ApiResponse({ + status: HttpStatus.OK, + type: VerificationResponse, + description: "成功時のレスポンス", + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: "想定外のサーバーエラー", + }) + @ApiOperation({ operationId: "dataVerification" }) + async dataVerification( + @Body() body: VerificationRequest, + @Req() req: Request + ): Promise { + const context = makeContext("iko", "varification"); + + const inputFilePath = body.inputFilePath; + + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.dataVerification.name + } | params: { inputFilePath: ${inputFilePath}};` + ); + + try { + // 読み込みファイルのフルパス + const accountTransitionFileFullPath = + inputFilePath + "Account_transition.csv"; + const accountMapFileFullPath = inputFilePath + "account_map.json"; + const cardLicensesFileFullPath = inputFilePath + "cardLicenses.json"; + + // ファイル存在チェックと読み込み + if (!fs.existsSync(accountTransitionFileFullPath)) { + this.logger.error( + `file not exists from ${accountTransitionFileFullPath}` + ); + throw new Error( + `file not exists from ${accountTransitionFileFullPath}` + ); + } + + if (!fs.existsSync(accountMapFileFullPath)) { + this.logger.error(`file not exists from ${accountMapFileFullPath}`); + throw new Error(`file not exists from ${accountMapFileFullPath}`); + } + + if (!fs.existsSync(cardLicensesFileFullPath)) { + this.logger.error(`file not exists from ${cardLicensesFileFullPath}`); + throw new Error(`file not exists from ${cardLicensesFileFullPath}`); + } + + // カードライセンスの登録用ファイル読み込み + const cardLicensesObject = JSON.parse( + fs.readFileSync(cardLicensesFileFullPath, "utf8") + ); + + // 型ガード(cardLicenses) + if (!isCardLicensesFileArray(cardLicensesObject)) { + throw new Error("input file is not cardLicensesInputFiles"); + } + + // アカウントIDマッピング用ファイル読み込み + const accountsMapObject = JSON.parse( + fs.readFileSync(accountMapFileFullPath, "utf8") + ); + + // 型ガード(accountsMapingFile) + if (!isAccountsMappingFileArray(accountsMapObject)) { + throw new Error("input file is not accountsMapingFile"); + } + + // 移行用csvファイルの読み込み(csv parse) + fs.createReadStream(accountTransitionFileFullPath).pipe( + csv.parse({ columns: true, delimiter: "," }, (err, csvInputFiles) => { + // 型ガード(csvInputFile) + if (!isCsvInputFileForValidateArray(csvInputFiles)) { + throw new Error("input file is not csvInputFile"); + } + + const csvInputFileswithRows: csvInputFileWithRow[] = []; + let rowCount = 2; // csvの何行目かを表す変数。ヘッダ行があるので2から開始 + for (const csvInputFile of csvInputFiles) { + const csvInputFileswithRow: csvInputFileWithRow = { + ...csvInputFile, + row: rowCount + }; + csvInputFileswithRows.push(csvInputFileswithRow); + rowCount = rowCount + 1; + } + this.verificationService.varificationData( + context, + inputFilePath, + csvInputFileswithRows, + accountsMapObject, + cardLicensesObject + ); + }) + ); + + return {}; + } 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.dataVerification.name}` + ); + } + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/data_migration_tools/server/src/features/verification/verification.module.ts b/data_migration_tools/server/src/features/verification/verification.module.ts new file mode 100644 index 0000000..51b9892 --- /dev/null +++ b/data_migration_tools/server/src/features/verification/verification.module.ts @@ -0,0 +1,17 @@ +import { Module } from "@nestjs/common"; +import { VerificationController } from "./verification.controller"; +import { VerificationService } from "./verification.service"; +import { LicensesRepositoryModule } from "../../repositories/licenses/licenses.repository.module"; +import { AccountsRepositoryModule } from "../../repositories/accounts/accounts.repository.module"; +import { UsersRepositoryModule } from "../../repositories//users/users.repository.module"; + +@Module({ + imports: [ + LicensesRepositoryModule, + AccountsRepositoryModule, + UsersRepositoryModule, + ], + controllers: [VerificationController], + providers: [VerificationService], +}) +export class VerificationModule {} diff --git a/data_migration_tools/server/src/features/verification/verification.service.ts b/data_migration_tools/server/src/features/verification/verification.service.ts new file mode 100644 index 0000000..4fda7d7 --- /dev/null +++ b/data_migration_tools/server/src/features/verification/verification.service.ts @@ -0,0 +1,726 @@ +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 !== "9999/12/31 23:59:59" + ).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] = (duplicates[str] || 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 !== "9999/12/31 23:59:59" + ), + 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; +} diff --git a/data_migration_tools/server/src/gateways/adb2c/adb2c.service.ts b/data_migration_tools/server/src/gateways/adb2c/adb2c.service.ts index 5ba0f0e..afb2ab2 100644 --- a/data_migration_tools/server/src/gateways/adb2c/adb2c.service.ts +++ b/data_migration_tools/server/src/gateways/adb2c/adb2c.service.ts @@ -64,41 +64,57 @@ export class AdB2cService { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.createUser.name}` ); - try { - // ユーザをADB2Cに登録 - const newUser = await this.graphClient.api("users/").post({ - accountEnabled: true, - displayName: username, - passwordPolicies: "DisableStrongPassword", - passwordProfile: { - forceChangePasswordNextSignIn: false, - password: password, - }, - identities: [ - { - signinType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, - issuer: `${this.tenantName}.onmicrosoft.com`, - issuerAssignedId: email, + + const retryCount: number = 3; + let retry = 0; + + while (retry < retryCount) { + try { + // ユーザをADB2Cに登録 + const newUser = await this.graphClient.api("users/").post({ + accountEnabled: true, + displayName: username, + passwordPolicies: "DisableStrongPassword", + passwordProfile: { + forceChangePasswordNextSignIn: false, + password: password, }, - ], - }); - return { sub: newUser.id }; - } catch (e) { - this.logger.error(`[${context.getTrackingId()}] error=${e}`); - if (e?.statusCode === 400 && e?.body) { - const error = JSON.parse(e.body); + identities: [ + { + signinType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: `${this.tenantName}.onmicrosoft.com`, + issuerAssignedId: email, + }, + ], + }); + this.logger.log( + `[${context.getTrackingId()}] [ADB2C CREATE] newUser: ${newUser}` + ); + return { sub: newUser.id }; + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + + if (e?.statusCode === 400 && e?.body) { + const error = JSON.parse(e.body); - // エラーが競合エラーである場合は、メールアドレス重複としてエラーを返す - if (error?.details?.find((x) => x.code === "ObjectConflict")) { - return { reason: "email", message: "ObjectConflict" }; + // エラーが競合エラーである場合は、メールアドレス重複としてエラーを返す + if (error?.details?.find((x) => x.code === "ObjectConflict")) { + return { reason: "email", message: "ObjectConflict" }; + } } - } - throw e; - } finally { - this.logger.log( - `[OUT] [${context.getTrackingId()}] ${this.createUser.name}` - ); + if (++retry < retryCount) { + this.logger.log(`ADB2Cエラー発生。5秒sleepしてリトライします (${retry}/${retryCount})...`); + await new Promise(resolve => setTimeout(resolve, 5000)); + } else { + this.logger.log(`リトライ数が上限に達したのでエラーを返却します`); + throw e; + } + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.createUser.name}` + ); + } } } diff --git a/data_migration_tools/server/src/repositories/accounts/accounts.repository.service.ts b/data_migration_tools/server/src/repositories/accounts/accounts.repository.service.ts index b38f6ba..d73563a 100644 --- a/data_migration_tools/server/src/repositories/accounts/accounts.repository.service.ts +++ b/data_migration_tools/server/src/repositories/accounts/accounts.repository.service.ts @@ -28,6 +28,7 @@ export class AccountsRepositoryService { * @param tier * @param adminExternalUserId * @param adminUserRole + * @param adminUserAuthId * @param accountId * @param userId * @param adminUserAcceptedEulaVersion @@ -43,6 +44,7 @@ export class AccountsRepositoryService { tier: number, adminExternalUserId: string, adminUserRole: string, + adminUserAuthId: string, accountId: number, userId: number, adminUserAcceptedEulaVersion?: string, @@ -75,6 +77,7 @@ export class AccountsRepositoryService { user.account_id = persistedAccount.id; user.external_id = adminExternalUserId; user.role = adminUserRole; + user.author_id = adminUserAuthId; user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null; user.accepted_privacy_notice_version = adminUserAcceptedPrivacyNoticeVersion ?? null; @@ -160,4 +163,21 @@ export class AccountsRepositoryService { ); }); } + + /** + * アカウント情報を全件取得する + * @returns Account[] + */ + async getAllAccounts( + context: Context, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountsRepo = entityManager.getRepository(Account); + + const accouts = accountsRepo.find({ + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + return accouts; + }); + } } diff --git a/data_migration_tools/server/src/repositories/licenses/licenses.repository.service.ts b/data_migration_tools/server/src/repositories/licenses/licenses.repository.service.ts index 643b7f4..7a9f46d 100644 --- a/data_migration_tools/server/src/repositories/licenses/licenses.repository.service.ts +++ b/data_migration_tools/server/src/repositories/licenses/licenses.repository.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from "@nestjs/common"; -import { DataSource, In } from "typeorm"; +import { DataSource } from "typeorm"; import { License, LicenseAllocationHistory, @@ -8,15 +8,10 @@ import { } from "./entity/license.entity"; import { insertEntity, insertEntities } from "../../common/repository"; import { Context } from "../../common/log"; -import { - LicensesInputFile, - CardLicensesInputFile, -} from "../../common/types/types"; -import {AUTO_INCREMENT_START} from "../../constants/index" -import { - LICENSE_ALLOCATED_STATUS, - LICENSE_TYPE, -} from "../../constants"; + +import { AUTO_INCREMENT_START } from "../../constants/index"; +import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from "../../constants"; +import { CardLicensesFile, LicensesFile } from "src/common/types/types"; @Injectable() export class LicensesRepositoryService { //クエリログにコメントを出力するかどうか @@ -27,27 +22,27 @@ export class LicensesRepositoryService { /** * ライセンスを登録する * @context Context - * @param licensesInputFiles + * @param LicensesFiles */ async insertLicenses( context: Context, - licensesInputFiles: LicensesInputFile[] + LicensesFiles: LicensesFile[] ): Promise<{}> { const nowDate = new Date(); return await this.dataSource.transaction(async (entityManager) => { const licenseRepo = entityManager.getRepository(License); let newLicenses: License[] = []; - licensesInputFiles.forEach((licensesInputFile) => { + LicensesFiles.forEach((LicensesFile) => { const license = new License(); - license.account_id = licensesInputFile.account_id; - license.status = licensesInputFile.status; - license.type = licensesInputFile.type; - license.expiry_date = licensesInputFile.expiry_date - ? new Date(licensesInputFile.expiry_date) + license.account_id = LicensesFile.account_id; + license.status = LicensesFile.status; + license.type = LicensesFile.type; + license.expiry_date = LicensesFile.expiry_date + ? new Date(LicensesFile.expiry_date) : null; - if (licensesInputFile.allocated_user_id) { - license.allocated_user_id = licensesInputFile.allocated_user_id; + if (LicensesFile.allocated_user_id) { + license.allocated_user_id = LicensesFile.allocated_user_id; } newLicenses.push(license); }); @@ -96,11 +91,11 @@ export class LicensesRepositoryService { /** * カードライセンスを登録する * @context Context - * @param cardLicensesInputFiles + * @param cardLicensesFiles */ async insertCardLicenses( context: Context, - cardLicensesInputFiles: CardLicensesInputFile[] + cardLicensesFiles: CardLicensesFile[] ): Promise<{}> { return await this.dataSource.transaction(async (entityManager) => { const cardLicenseRepo = entityManager.getRepository(CardLicense); @@ -110,7 +105,7 @@ export class LicensesRepositoryService { const licenses: License[] = []; // ライセンステーブルを作成する(BULK INSERT) - for (let i = 0; i < cardLicensesInputFiles.length; i++) { + for (let i = 0; i < cardLicensesFiles.length; i++) { const license = new License(); license.account_id = AUTO_INCREMENT_START; // 最初に登場するアカウント(第一アカウント) license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED; @@ -139,23 +134,22 @@ export class LicensesRepositoryService { const newCardLicenses: CardLicense[] = []; // カードライセンステーブルを作成する(BULK INSERT) - for (let i = 0; i < cardLicensesInputFiles.length; i++) { + for (let i = 0; i < cardLicensesFiles.length; i++) { const cardLicense = new CardLicense(); cardLicense.license_id = savedLicenses[i].id; // Licenseテーブルの自動採番されたIDを挿入 cardLicense.issue_id = savedCardLicensesIssue.id; // CardLicenseIssueテーブルの自動採番されたIDを挿入 - cardLicense.card_license_key = - cardLicensesInputFiles[i].card_license_key; - cardLicense.activated_at = cardLicensesInputFiles[i].activated_at - ? new Date(cardLicensesInputFiles[i].activated_at) + cardLicense.card_license_key = cardLicensesFiles[i].card_license_key; + cardLicense.activated_at = cardLicensesFiles[i].activated_at + ? new Date(cardLicensesFiles[i].activated_at) : null; - cardLicense.created_at = cardLicensesInputFiles[i].created_at - ? new Date(cardLicensesInputFiles[i].created_at) + cardLicense.created_at = cardLicensesFiles[i].created_at + ? new Date(cardLicensesFiles[i].created_at) : null; - cardLicense.created_by = cardLicensesInputFiles[i].created_by; - cardLicense.updated_at = cardLicensesInputFiles[i].updated_at - ? new Date(cardLicensesInputFiles[i].updated_at) + cardLicense.created_by = cardLicensesFiles[i].created_by; + cardLicense.updated_at = cardLicensesFiles[i].updated_at + ? new Date(cardLicensesFiles[i].updated_at) : null; - cardLicense.updated_by = cardLicensesInputFiles[i].updated_by; + cardLicense.updated_by = cardLicensesFiles[i].updated_by; newCardLicenses.push(cardLicense); } @@ -172,4 +166,34 @@ export class LicensesRepositoryService { return {}; }); } + + /** + * ライセンス情報を全件取得する + * @returns License[] + */ + async getAllLicenses(context: Context): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const licenseRepo = entityManager.getRepository(License); + + const licenses = licenseRepo.find({ + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + return licenses; + }); + } + + /** + * カードライセンス情報を全件取得する + * @returns CardLicense[] + */ + async getAllCardLicense(context: Context): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const cardLicenseRepo = entityManager.getRepository(CardLicense); + + const cardLicenses = cardLicenseRepo.find({ + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + return cardLicenses; + }); + } } diff --git a/data_migration_tools/server/src/repositories/users/users.repository.service.ts b/data_migration_tools/server/src/repositories/users/users.repository.service.ts index ebb4dc3..d060de7 100644 --- a/data_migration_tools/server/src/repositories/users/users.repository.service.ts +++ b/data_migration_tools/server/src/repositories/users/users.repository.service.ts @@ -138,4 +138,20 @@ export class UsersRepositoryService { await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context); }); } + + /** + * ユーザー情報を全件取得する + * @returns User[] + */ + async getAllUsers(context: Context): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const userRepo = entityManager.getRepository(User); + + const users = userRepo.find({ + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + return users; + }); + } } + diff --git a/data_migration_tools/server/src/repositories/worktypes/worktypes.repository.service.ts b/data_migration_tools/server/src/repositories/worktypes/worktypes.repository.service.ts index 9f7f2ff..3245b63 100644 --- a/data_migration_tools/server/src/repositories/worktypes/worktypes.repository.service.ts +++ b/data_migration_tools/server/src/repositories/worktypes/worktypes.repository.service.ts @@ -13,7 +13,7 @@ import { import { OptionItem } from "./entity/option_item.entity"; import { insertEntities, insertEntity } from "../../common/repository"; import { Context } from "../../common/log"; -import { WorktypesInputFile } from "../../common/types/types"; +import { WorktypesFile } from "../../common/types/types"; @Injectable() export class WorktypesRepositoryService { @@ -30,15 +30,15 @@ export class WorktypesRepositoryService { */ async createWorktype( context: Context, - worktypesInputFiles: WorktypesInputFile[] + WorktypesFiles: WorktypesFile[] ): Promise { await this.dataSource.transaction(async (entityManager) => { const worktypeRepo = entityManager.getRepository(Worktype); const optionItemRepo = entityManager.getRepository(OptionItem); - for (const worktypesInputFile of worktypesInputFiles) { - const accountId = worktypesInputFile.account_id; - const worktypeId = worktypesInputFile.custom_worktype_id; + for (const WorktypesFile of WorktypesFiles) { + const accountId = WorktypesFile.account_id; + const worktypeId = WorktypesFile.custom_worktype_id; const description = null; const duplicatedWorktype = await worktypeRepo.findOne({ diff --git a/dictation_server/src/common/password/password.ts b/dictation_server/src/common/password/password.ts index f68bb3c..72950b8 100644 --- a/dictation_server/src/common/password/password.ts +++ b/dictation_server/src/common/password/password.ts @@ -15,9 +15,12 @@ export const makePassword = (): string => { // autoGeneratedPasswordが以上の条件を満たせばvalidがtrueになる let valid = false; - let autoGeneratedPassword: string = ''; + let autoGeneratedPassword = ''; while (!valid) { + // 再生成用に変数を初期化する + autoGeneratedPassword = ''; + // パスワードをランダムに決定 while (autoGeneratedPassword.length < passLength) { // 上で決定したcharsの中からランダムに1文字ずつ追加 diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 1951519..ecefac5 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -950,6 +950,72 @@ describe('TasksService', () => { expect(task.jobNumber).toEqual('00000001'); } }); + it('[Admin] Taskが100件であっても取得できる', async () => { + const notificationhubServiceMockValue = + makeDefaultNotificationhubServiceMockValue(); + if (!source) fail(); + const module = await makeTaskTestingModuleWithNotificaiton( + source, + notificationhubServiceMockValue, + ); + if (!module) fail(); + const { id: accountId } = await makeTestSimpleAccount(source); + const { external_id } = await makeTestUser(source, { + account_id: accountId, + external_id: 'userId', + role: 'none', + }); + const { id: authorUserId, author_id } = await makeTestUser(source, { + account_id: accountId, + external_id: 'userId', + author_id: 'MY_AUTHOR_ID', + role: 'author', + }); + + const service = module.get(TasksService); + for (let i = 0; i < 100; i++) { + await createTask( + source, + accountId, + authorUserId, + author_id ?? '', + `WORKTYPE${i + 1}`, + '01', + // 00000001 ~ 00000100 + `000000${String(i + 1).padStart(2, '0')}`, + 'Uploaded', + ); + } + const offset = 0; + const limit = 100; + const status = ['Uploaded', 'Backup']; + const paramName = 'WORK_TYPE'; + const direction = 'DESC'; + + const { tasks, total } = await service.getTasks( + makeContext('trackingId', 'requestId'), + external_id, + [ADMIN_ROLES.ADMIN, USER_ROLES.NONE], + offset, + limit, + status, + paramName, + direction, + ); + expect(tasks.length).toEqual(100); + expect(total).toEqual(100); + // ソート条件がWORK_TYPEのため、WORK_TYPEが降順になっていることを確認 + expect(tasks[0].workType).toEqual('WORKTYPE99'); + expect(tasks[99].workType).toEqual('WORKTYPE1'); + expect(tasks[0].optionItemList).toEqual( + Array.from({ length: 10 }).map((_, i) => { + return { + optionItemLabel: `label${i}:audio_file_id${tasks[0].audioFileId}`, + optionItemValue: `value${i}:audio_file_id${tasks[0].audioFileId}`, + }; + }), + ); + }); }); }); diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts index 48dd0bb..11b42ea 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -438,12 +438,14 @@ export class TasksService { `author_id not found. audioFileId: ${audioFileId}. account_id: ${user.account_id}`, ); } - const { external_id: authorExternalId } = - await this.usersRepository.findUserByAuthorId( - context, - task.file.author_id, - user.account_id, - ); + const { + external_id: authorExternalId, + notification: authorNotification, + } = await this.usersRepository.findUserByAuthorId( + context, + task.file.author_id, + user.account_id, + ); // プライマリ管理者を取得 const { external_id: primaryAdminExternalId } = @@ -457,6 +459,7 @@ export class TasksService { ]); // メール送信に必要な情報を取得 + // Author通知ON/OFF関わらずAuthor名は必要なため、情報の取得は行う const author = usersInfo.find((x) => x.id === authorExternalId); if (!author) { throw new Error(`author not found. id=${authorExternalId}`); @@ -491,7 +494,7 @@ export class TasksService { // メール送信 this.sendgridService.sendMailWithU117( context, - authorEmail, + authorNotification ? authorEmail : null, typistEmail, authorName, task.file.file_name.replace('.zip', ''), diff --git a/dictation_server/src/features/tasks/types/convert.ts b/dictation_server/src/features/tasks/types/convert.ts index 5353542..2ce6d99 100644 --- a/dictation_server/src/features/tasks/types/convert.ts +++ b/dictation_server/src/features/tasks/types/convert.ts @@ -79,12 +79,18 @@ const createTask = ( const createAudioOptionItems = ( optionItems: AudioOptionItemEntity[], ): AudioOptionItem[] => { - return optionItems.map((x) => { - return { - optionItemLabel: x.label, - optionItemValue: x.value, - }; - }); + // バグ 3786: [FB対応]タスク一覧画面のOptionItemがソート条件によって表示順がおかしくなる の対応 + // 並び順をID順に固定する + // 本来はRepository側でソートするべきだが、TYPEORMの仕様でソートすると取得件数が想定通りに取得できないため、ここでソートする + // 詳細は タスク 3815: タスク一覧画面の取得件数が10件となっているバグの対応 + return optionItems + .sort((a: AudioOptionItemEntity, b: AudioOptionItemEntity) => a.id - b.id) + .map((x) => { + return { + optionItemLabel: x.label, + optionItemValue: x.value, + }; + }); }; // Repository側のDTOからAssigneeオブジェクトを構築する diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index e960d25..b21e914 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -1051,7 +1051,7 @@ export class SendGridService { */ async sendMailWithU117( context: Context, - authorEmail: string, + authorEmail: string | null, typistEmail: string, authorName: string, fileName: string, @@ -1079,7 +1079,7 @@ export class SendGridService { // メールを送信する await this.sendMail( context, - [authorEmail, typistEmail], + [authorEmail, typistEmail].filter((x): x is string => x !== null), // authorEmailがnullの場合は除外する [], this.mailFrom, subject, diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 4302a9b..d712597 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -1582,91 +1582,78 @@ const makeOrder = ( priority: 'DESC', job_number: direction, id: 'ASC', - option_items: { id: 'ASC' }, }; case 'STATUS': return { priority: 'DESC', status: direction, id: 'ASC', - option_items: { id: 'ASC' }, }; case 'TRANSCRIPTION_FINISHED_DATE': return { priority: 'DESC', finished_at: direction, id: 'ASC', - option_items: { id: 'ASC' }, }; case 'TRANSCRIPTION_STARTED_DATE': return { priority: 'DESC', started_at: direction, id: 'ASC', - option_items: { id: 'ASC' }, }; case 'AUTHOR_ID': return { priority: 'DESC', file: { author_id: direction }, id: 'ASC', - option_items: { id: 'ASC' }, }; case 'ENCRYPTION': return { priority: 'DESC', file: { is_encrypted: direction }, id: 'ASC', - option_items: { id: 'ASC' }, }; case 'FILE_LENGTH': return { priority: 'DESC', file: { duration: direction }, id: 'ASC', - option_items: { id: 'ASC' }, }; case 'FILE_NAME': return { priority: 'DESC', file: { file_name: direction }, id: 'ASC', - option_items: { id: 'ASC' }, }; case 'FILE_SIZE': return { priority: 'DESC', file: { file_size: direction }, id: 'ASC', - option_items: { id: 'ASC' }, }; case 'RECORDING_FINISHED_DATE': return { priority: 'DESC', file: { finished_at: direction }, id: 'ASC', - option_items: { id: 'ASC' }, }; case 'RECORDING_STARTED_DATE': return { priority: 'DESC', file: { started_at: direction }, id: 'ASC', - option_items: { id: 'ASC' }, }; case 'UPLOAD_DATE': return { priority: 'DESC', file: { uploaded_at: direction }, id: 'ASC', - option_items: { id: 'ASC' }, }; case 'WORK_TYPE': return { priority: 'DESC', file: { work_type_id: direction }, id: 'ASC', - option_items: { id: 'ASC' }, }; default: // switchのcase漏れが発生した場合に型エラーになるようにする