Merge branch 'develop'

This commit is contained in:
SAITO-PC-3\saito.k 2024-02-27 18:53:10 +09:00
commit 34d1dd5629
33 changed files with 1005 additions and 611 deletions

View File

@ -1,70 +0,0 @@
/*
E+6
- 1~2...
- 3~4DB...
- 5~6
ex)
E00XXXX : システムエラーDB接続失敗など
E01XXXX : 業務エラー
EXX00XX : 内部エラー
EXX01XX : トークンエラー
EXX02XX : DBエラーDB関連
EXX03XX : ADB2CエラーDB関連
*/
export const ErrorCodes = [
'E009999', // 汎用エラー
'E000101', // トークン形式不正エラー
'E000102', // トークン有効期限切れエラー
'E000103', // トークン非アクティブエラー
'E000104', // トークン署名エラー
'E000105', // トークン発行元エラー
'E000106', // トークンアルゴリズムエラー
'E000107', // トークン不足エラー
'E000108', // トークン権限エラー
'E000301', // ADB2Cへのリクエスト上限超過エラー
'E000401', // IPアドレス未設定エラー
'E000501', // リクエストID未設定エラー
'E010001', // パラメータ形式不正エラー
'E010201', // 未認証ユーザエラー
'E010202', // 認証済ユーザエラー
'E010203', // 管理ユーザ権限エラー
'E010204', // ユーザ不在エラー
'E010205', // DBのRoleが想定外の値エラー
'E010206', // DBのTierが想定外の値エラー
'E010207', // ユーザーのRole変更不可エラー
'E010208', // ユーザーの暗号化パスワード不足エラー
'E010209', // ユーザーの同意済み利用規約バージョンが最新でないエラー
'E010301', // メールアドレス登録済みエラー
'E010302', // authorId重複エラー
'E010401', // PONumber重複エラー
'E010501', // アカウント不在エラー
'E010502', // アカウント情報変更不可エラー
'E010503', // 代行操作不許可エラー
'E010504', // アカウントロックエラー
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
'E010602', // タスク変更権限不足エラー
'E010603', // タスク不在エラー
'E010701', // Blobファイル不在エラー
'E010801', // ライセンス不在エラー
'E010802', // ライセンス取り込み済みエラー
'E010803', // ライセンス発行済みエラー
'E010804', // ライセンス不足エラー
'E010805', // ライセンス有効期限切れエラー
'E010806', // ライセンス割り当て不可エラー
'E010807', // ライセンス割り当て解除済みエラー
'E010808', // ライセンス注文キャンセル不可エラー
'E010809', // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
'E010810', // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
'E010812', // ライセンス未割当エラー
'E010908', // タイピストグループ不在エラー
'E010909', // タイピストグループ名重複エラー
'E011001', // ワークタイプ重複エラー
'E011002', // ワークタイプ登録上限超過エラー
'E011003', // ワークタイプ不在エラー
'E011004', // ワークタイプ使用中エラー
'E012001', // テンプレートファイル不在エラー
'E013001', // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
'E013002', // ワークフロー不在エラー
] as const;

View File

@ -1,10 +0,0 @@
import { errors } from './message';
import { ErrorCodeType, ErrorResponse } from './types/types';
export const makeErrorResponse = (errorcode: ErrorCodeType): ErrorResponse => {
const msg = errors[errorcode];
return {
code: errorcode,
message: msg,
};
};

View File

@ -1,59 +0,0 @@
import { Errors } from './types/types';
// エラーコードとメッセージ対応表
export const errors: Errors = {
E009999: 'Internal Server Error.',
E000101: 'Token invalid format Error.',
E000102: 'Token expired Error.',
E000103: 'Token not before Error',
E000104: 'Token invalid signature Error.',
E000105: 'Token invalid issuer Error.',
E000106: 'Token invalid algorithm Error.',
E000107: 'Token is not exist Error.',
E000108: 'Token authority failed Error.',
E000301: 'ADB2C request limit exceeded Error',
E000401: 'IP address not found Error.',
E000501: 'Request ID not found Error.',
E010001: 'Param invalid format Error.',
E010201: 'Email not verified user Error.',
E010202: 'Email already verified user Error.',
E010203: 'Administrator Permissions Error.',
E010204: 'User not Found Error.',
E010205: 'Role from DB is unexpected value Error.',
E010206: 'Tier from DB is unexpected value Error.',
E010207: 'User role change not allowed Error.',
E010208: 'User encryption password not found Error.',
E010209: 'Accepted term not latest Error.',
E010301: 'This email user already created Error',
E010302: 'This AuthorId already used Error',
E010401: 'This PoNumber already used Error',
E010501: 'Account not Found Error.',
E010502: 'Account information cannot be changed Error.',
E010503: 'Delegation not allowed Error.',
E010504: 'Account is locked Error.',
E010601: 'Task is not Editable Error',
E010602: 'No task edit permissions Error',
E010603: 'Task not found Error.',
E010701: 'File not found in Blob Storage Error.',
E010801: 'License not exist Error',
E010802: 'License already activated Error',
E010803: 'License already issued Error',
E010804: 'License shortage Error',
E010805: 'License is expired Error',
E010806: 'License is unavailable Error',
E010807: 'License is already deallocated Error',
E010808: 'Order cancel failed Error',
E010809: 'Already license order status changed Error',
E010810: 'Cancellation period expired error',
E010811: 'Already license allocated Error',
E010812: 'License not allocated Error',
E010908: 'Typist Group not exist Error',
E010909: 'Typist Group name already exist Error',
E011001: 'This WorkTypeID already used Error',
E011002: 'WorkTypeID create limit exceeded Error',
E011003: 'WorkTypeID not found Error',
E011004: 'WorkTypeID is in use Error',
E012001: 'Template file not found Error',
E013001: 'AuthorId and WorktypeId pair already exists Error',
E013002: 'Workflow not found Error',
};

View File

@ -1,15 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { ErrorCodes } from '../code';
export class ErrorResponse {
@ApiProperty()
message: string;
@ApiProperty()
code: string;
}
export type ErrorCodeType = (typeof ErrorCodes)[number];
export type Errors = {
[P in ErrorCodeType]: string;
};

View File

@ -3107,9 +3107,9 @@
}
},
"node_modules/acorn": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"devOptional": true,
"bin": {
"acorn": "bin/acorn"
@ -12333,9 +12333,9 @@
}
},
"acorn": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"devOptional": true
},
"acorn-import-assertions": {

View File

@ -24,7 +24,9 @@ import { DeleteModule } from "./features/delete/delete.module";
import { DeleteRepositoryModule } from "./repositories/delete/delete.repository.module";
import { DeleteController } from "./features/delete/delete.controller";
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";
@Module({
imports: [
ServeStaticModule.forRoot({
@ -37,6 +39,7 @@ import { DeleteService } from "./features/delete/delete.service";
AdB2cModule,
AccountsModule,
UsersModule,
TransferModule,
RegisterModule,
AccountsRepositoryModule,
UsersRepositoryModule,
@ -61,8 +64,20 @@ import { DeleteService } from "./features/delete/delete.service";
inject: [ConfigService],
}),
],
controllers: [RegisterController, AccountsController, UsersController, DeleteController],
providers: [RegisterService, AccountsService, UsersService, DeleteService],
controllers: [
RegisterController,
AccountsController,
UsersController,
DeleteController,
TransferController,
],
providers: [
RegisterService,
AccountsService,
UsersService,
DeleteService,
TransferService,
],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {

View File

@ -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,7 @@ export class csvInputFile {
wt19: string;
wt20: string;
}
export class AccountsOutputFileStep1 {
export class AccountsFileType {
accountId: number;
type: string;
companyName: string;
@ -43,9 +43,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 +56,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 +69,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 +77,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 +93,10 @@ export class CardLicensesInputFile {
updated_by?: string;
}
export function isAccountsInputFileArray(obj: any): obj is AccountsInputFile[] {
return Array.isArray(obj) && obj.every((item) => isAccountsInputFile(item));
export function isAccountsFileArray(obj: any): obj is AccountsFile[] {
return Array.isArray(obj) && obj.every((item) => isAccountsFile(item));
}
export function isAccountsInputFile(obj: any): obj is AccountsInputFile {
export function isAccountsFile(obj: any): obj is AccountsFile {
return (
typeof obj === "object" &&
obj !== null &&
@ -134,21 +109,27 @@ export function isAccountsInputFile(obj: any): obj is AccountsInputFile {
"country" in obj &&
typeof obj.country === "string" &&
("dealerAccountId" in obj
? typeof obj.dealerAccountId === "number"
? obj.dealerAccountId === null || typeof obj.dealerAccountId === "number"
: true) &&
"adminName" in obj &&
typeof obj.adminName === "string" &&
"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 +148,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 +168,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 +182,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 &&

View File

@ -343,7 +343,7 @@ export const MIGRATION_TYPE = {
export const COUNTRY_LIST = [
{ value: "CA", label: "Canada" },
{ value: "KY", label: "Cayman Islands" },
{ value: "US", label: "U.S.A." },
{ value: "US", label: "United States" },
{ value: "AU", label: "Australia" },
{ value: "NZ", label: "New Zealand" },
{ value: "AT", label: "Austria" },
@ -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" },

View File

@ -37,6 +37,7 @@ export class AccountsService {
password: string,
username: string,
role: string,
authorId: string,
acceptedEulaVersion: string,
acceptedPrivacyNoticeVersion: string,
acceptedDpaVersion: string,
@ -103,6 +104,7 @@ export class AccountsService {
type,
externalUser.sub,
role,
authorId,
accountId,
userId,
acceptedEulaVersion,

View File

@ -11,6 +11,7 @@ import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { Request } from "express";
import { DeleteService } from "./delete.service";
import { DeleteResponse } from "./types/types";
import { makeContext } from "src/common/log";
@ApiTags("delete")
@Controller("delete")
@ -33,7 +34,9 @@ export class DeleteController {
})
@Post()
async deleteData(): Promise<{}> {
await this.deleteService.deleteData();
const context = makeContext("tool", "delete");
await this.deleteService.deleteData(context);
return {};
}
}

View File

@ -3,6 +3,7 @@ import { DeleteRepositoryService } from "../../repositories/delete/delete.reposi
import { makeErrorResponse } from "../../common/errors/makeErrorResponse";
import { AdB2cService } from "../../gateways/adb2c/adb2c.service";
import { BlobstorageService } from "../../gateways/blobstorage/blobstorage.service";
import { Context } from "../../common/log";
@Injectable()
export class DeleteService {
@ -11,27 +12,41 @@ export class DeleteService {
private readonly deleteRepositoryService: DeleteRepositoryService,
private readonly blobstorageService: BlobstorageService,
private readonly adB2cService: AdB2cService
) {}
) { }
/**
*
* @returns data
*/
async deleteData(): Promise<void> {
this.logger.log(`[IN] ${this.deleteData.name}`);
async deleteData(context: Context): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.deleteData.name}`
);
try {
// BlobStorageからデータを削除する
await this.blobstorageService.deleteContainers();
await this.blobstorageService.deleteContainers(context);
// ADB2Cからユーザ情報を取得する
const users = await this.adB2cService.getUsers();
const externalIds = users.map((user) => user.id);
await this.adB2cService.deleteUsers(externalIds);
// 100件ずつのユーザー取得なのですべて削除するまでループする
for (let i = 0; i < 500; i++) {
// ADB2Cからユーザ情報を取得する
const { users, hasNext } = await this.adB2cService.getUsers(context);
const externalIds = users.map((user) => user.id);
await this.adB2cService.deleteUsers(context, externalIds);
// 削除していないユーザーがいない場合はループを抜ける
if (!hasNext) {
break;
}
}
// データベースからデータを削除する
await this.deleteRepositoryService.deleteData();
// AutoIncrementの値をリセットする
await this.deleteRepositoryService.resetAutoIncrement();
// 初期データを挿入する
await this.deleteRepositoryService.insertInitData(context);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {

View File

@ -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,50 @@ 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) {
// ランダムなパスワードを生成する
const ramdomPassword = makePassword();
// 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");
}
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 +152,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 +184,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 +194,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 +204,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");
}
// ライセンス・ワークタイプ・カードライセンスの登録

View File

@ -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<void> {
// パラメータ内容が長大なのでログには出さない
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) {

View File

@ -11,25 +11,16 @@ import fs from "fs";
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { Request } from "express";
import { transferRequest, transferResponse } from "./types/types";
import { transferService } from "./transfer.service";
import { TransferService } from "./transfer.service";
import { makeContext } from "../../common/log";
import { csvInputFile } 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 "../../../src/constants";
import { AUTO_INCREMENT_START } from "../../constants";
@ApiTags("transfer")
@Controller("transfer")
export class transferController {
private readonly logger = new Logger(transferController.name);
constructor(private readonly transferService: transferService) {}
export class TransferController {
private readonly logger = new Logger(TransferController.name);
constructor(private readonly transferService: TransferService) {}
@Post()
@ApiResponse({
@ -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,7 +68,34 @@ 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, "");
});
// "\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
);
}
csvInputFile.push({
type: data[0],
account_id: data[1],
@ -88,8 +106,8 @@ export class transferController {
last_name: data[6],
country: data[7],
state: data[8],
start_date: new Date(data[9]),
expired_date: new Date(data[10]),
start_date: data[9],
expired_date: data[10],
user_email: data[11],
author_id: data[12],
recording_mode: data[13],
@ -115,6 +133,10 @@ export class transferController {
wt20: data[33],
});
});
// 最後の行がundefinedの場合はその行を削除
if (csvInputFile[csvInputFile.length - 1].account_id === undefined) {
csvInputFile.pop();
}
// 各データのバリデーションチェック
await this.transferService.validateInputData(context, csvInputFile);
@ -128,35 +150,39 @@ export class transferController {
accountIdMap.set(accountId, index + AUTO_INCREMENT_START);
});
// 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をつのJSONファイルの出力する(出力先はinputと同じにする)
// transferResponseCsvつの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,
resultDuplicateEmail.usersFileLines,
resultDuplicateEmail.licensesFileLines,
WorktypesFile
);
return {};
} catch (e) {

View File

@ -1,9 +1,9 @@
import { Module } from "@nestjs/common";
import { transferController } from "./transfer.controller";
import { transferService } from "./transfer.service";
import { TransferController } from "./transfer.controller";
import { TransferService } from "./transfer.service";
@Module({
imports: [],
controllers: [transferController],
providers: [transferService],
controllers: [TransferController],
providers: [TransferService],
})
export class transferModule {}
export class TransferModule {}

View File

@ -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,42 +18,46 @@ 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 {
export class TransferService {
constructor() {}
private readonly logger = new Logger(transferService.name);
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<string, number>
): Promise<registInputDataResponse> {
// パラメータ内容が長大なのでログには出さない
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 userIdIndex = 0;
// authorIdとuserIdの対応関係を保持するMapを定義
const authorIdToUserIdMap: Map<string, number> = new Map();
// 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
@ -71,8 +75,16 @@ export class transferService {
if (line.parent_id) {
parentAccountId = accountIdMap.get(line.parent_id);
}
// AccountsOutputFile配列にPush
accountsOutputFileStep1Lines.push({
// 万が一parent_idが入力されているのに存在しなかった場合は、nullを設定する。
if (parentAccountId === undefined) {
parentAccountId = null;
}
// userIdIndexをインクリメントする
userIdIndex++;
// AccountsFile配列にPush
accountsFileTypeLines.push({
// accountIdはaccountIdMapから取得する
accountId: accountIdMap.get(line.account_id),
type: line.type,
@ -82,148 +94,176 @@ 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"の場合
if (line.type == MIGRATION_TYPE.USER) {
// 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.997のデータの場合はデモライセンスなので登録しない
if (line.expired_date !== "9999/12/31 23:59:59.997") {
// 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;
}
}
}
}
}
// つぎの行に進む
});
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<AccountsOutputFile[]> {
accountsFileType: AccountsFileType[]
): Promise<AccountsFile[]> {
// パラメータ内容が長大なのでログには出さない
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.relocateHierarchy.name}`
);
try {
// dealerAccountIdを検索し、typeがCountryの場合
accountsOutputFileStep1.forEach((account) => {
if (account.type === MIGRATION_TYPE.COUNTRY) {
// そのacccountIdをdealerAccountIdにもつアカウント(Distributor)を検索する
const distributor = accountsOutputFileStep1.find(
(distributor) =>
account.type === MIGRATION_TYPE.DISTRIBUTOR &&
distributor.dealerAccountId === account.accountId
);
// DistributorのdealerAccountIdをBCCountryの親に付け替える
distributor.dealerAccountId = account.dealerAccountId;
}
});
// typeがCountryのアカウントを取り除く
accountsOutputFileStep1 = accountsOutputFileStep1.filter(
(account) => account.type !== MIGRATION_TYPE.COUNTRY
);
const relocatedAccounts: AccountsFile[] = [];
const countryRecords: Map<number, number> = new Map();
// 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;
// accountsFileTypeをループ
accountsFileType.forEach((account) => {
// Countryの場合はDistributorのアカウントIDと新たな親アカウントIDBCの組み合わせをMapに登録
if (account.type === MIGRATION_TYPE.COUNTRY) {
// 配下のDistributorアカウントを取得
const distributor = accountsFileType.find(
(distributor) =>
distributor.dealerAccountId === account.accountId &&
distributor.type === MIGRATION_TYPE.DISTRIBUTOR
);
if (distributor) {
countryRecords.set(distributor.accountId, account.dealerAccountId);
}
} else {
// Country以外のアカウントの場合は、そのまま登録
countryRecords.set(account.accountId, account.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,
});
});
return accountsOutputFile;
// AccountsFileTypeのループを行い、階層情報の置換と新たな配列へのpushを行う
accountsFileType.forEach((account) => {
// Countryのレコードは除外する
if (account.type !== MIGRATION_TYPE.COUNTRY) {
const dealerAccountId =
countryRecords.get(account.dealerAccountId) ??
account.dealerAccountId;
const type = this.getAccountType(account.type);
const newAccount: AccountsFile = {
accountId: account.accountId,
type: type,
companyName: account.companyName,
country: account.country,
dealerAccountId: dealerAccountId,
adminName: account.adminName,
adminMail: account.adminMail,
userId: account.userId,
role: account.role,
authorId: account.authorId,
};
relocatedAccounts.push(newAccount);
}
});
return relocatedAccounts;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
makeErrorResponse("E009999"),
HttpStatus.INTERNAL_SERVER_ERROR
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.relocateHierarchy.name}`
@ -231,21 +271,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<void> {
// パラメータ内容が長大なのでログには出さない
this.logger.log(
@ -254,29 +311,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}`
@ -298,12 +350,15 @@ export class transferService {
);
try {
// アカウントに対するworktypeのMap配列を作成する
const accountWorktypeMap = new Map<string, string[]>();
// csvInputFileのバリデーションチェックを行う
csvInputFile.forEach((line, index) => {
// typeのバリデーションチェック
if (
line.type !== MIGRATION_TYPE.ADMINISTRATOR &&
line.type !== MIGRATION_TYPE.BC &&
line.type !== MIGRATION_TYPE.COUNTRY &&
line.type !== MIGRATION_TYPE.DISTRIBUTOR &&
line.type !== MIGRATION_TYPE.DEALER &&
line.type !== MIGRATION_TYPE.CUSTOMER &&
@ -315,43 +370,223 @@ export class transferService {
);
}
// countryのバリデーションチェック
if (!COUNTRY_LIST.find((country) => country.label === line.country)) {
throw new HttpException(
`country is invalid. index=${index} country=${line.country}`,
HttpStatus.BAD_REQUEST
);
if (line.country) {
if (!COUNTRY_LIST.find((country) => country.label === line.country)) {
throw new HttpException(
`country is invalid. index=${index} country=${line.country}`,
HttpStatus.BAD_REQUEST
);
}
}
// mailのバリデーションチェック
// メールアドレスの形式が正しいかどうかのチェック
const mailRegExp = new RegExp(
/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$/
);
if (!mailRegExp.test(line.email)) {
throw new HttpException(
`email is invalid. index=${index} email=${line.email}`,
HttpStatus.BAD_REQUEST
);
const mailRegExp =
/^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/;
if (line.email) {
if (!mailRegExp.test(line.email)) {
throw new HttpException(
`email is invalid. index=${index} email=${line.email}`,
HttpStatus.BAD_REQUEST
);
}
}
// recording_modeのバリデーションチェック
// RECORDING_MODEに存在するかどうかのチェック
if (
line.recording_mode !== RECORDING_MODE.DS2_QP &&
line.recording_mode !== RECORDING_MODE.DS2_SP &&
line.recording_mode !== RECORDING_MODE.DSS &&
line.recording_mode !== null
) {
throw new HttpException(
`recording_mode is invalid. index=${index} recording_mode=${line.recording_mode}`,
HttpStatus.BAD_REQUEST
);
if (line.user_email) {
if (!mailRegExp.test(line.user_email)) {
throw new HttpException(
`user_email is invalid. index=${index} user_email=${line.email}`,
HttpStatus.BAD_REQUEST
);
}
}
// recording_modeの値が存在する場合
if (line.recording_mode) {
// recording_modeのバリデーションチェック
if (
line.recording_mode !== RECORDING_MODE.DS2_QP &&
line.recording_mode !== RECORDING_MODE.DS2_SP &&
line.recording_mode !== RECORDING_MODE.DSS
) {
throw new HttpException(
`recording_mode is invalid. index=${index} recording_mode=${line.recording_mode}`,
HttpStatus.BAD_REQUEST
);
}
}
// worktypeの1アカウント20件上限チェック
for (let i = 1; i <= WORKTYPE_MAX_COUNT; i++) {
const wt = `wt${i}`;
if (line[wt]) {
if (accountWorktypeMap.has(line.account_id)) {
const worktypes = accountWorktypeMap.get(line.account_id);
// 重複している場合はPushしない
if (worktypes?.includes(line[wt])) {
continue;
} else {
worktypes?.push(line[wt]);
}
// 20件を超えたらエラー
if (worktypes?.length > WORKTYPE_MAX_COUNT) {
throw new HttpException(
`worktype is over. index=${index} account_id=${line.account_id}`,
HttpStatus.BAD_REQUEST
);
}
} else {
accountWorktypeMap.set(line.account_id, [line[wt]]);
}
}
}
});
} 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<removeDuplicateEmailResponse> {
// パラメータ内容が長大なのでログには出さない
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}`
);
}
}
}

View File

@ -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[];
}

View File

@ -30,14 +30,10 @@ export const isConflictError = (arg: unknown): arg is ConflictError => {
export class AdB2cService {
private readonly logger = new Logger(AdB2cService.name);
private readonly tenantName: string;
private readonly flowName: string;
private readonly ttl: number;
private graphClient: Client;
constructor(private readonly configService: ConfigService) {
this.tenantName = this.configService.getOrThrow<string>("TENANT_NAME");
this.flowName = this.configService.getOrThrow<string>("SIGNIN_FLOW_NAME");
this.ttl = this.configService.getOrThrow<number>("ADB2C_CACHE_TTL");
// ADB2Cへの認証情報
const credential = new ClientSecretCredential(
@ -111,8 +107,10 @@ export class AdB2cService {
* @param externalIds
* @returns users
*/
async getUsers(): Promise<AdB2cUser[]> {
this.logger.log(`[IN] ${this.getUsers.name}`);
async getUsers(
context: Context
): Promise<{ users: AdB2cUser[]; hasNext: boolean }> {
this.logger.log(`[IN] [${context.getTrackingId()}] ${this.getUsers.name}`);
try {
const res: AdB2cResponse = await this.graphClient
@ -121,7 +119,7 @@ export class AdB2cService {
.filter(`creationType eq 'LocalAccount'`)
.get();
return res.value;
return { users: res.value, hasNext: !!res["@odata.nextLink"] };
} catch (e) {
this.logger.error(`error=${e}`);
const { statusCode } = e;
@ -177,9 +175,11 @@ export class AdB2cService {
* Azure AD B2Cからユーザ情報を削除する
* @param externalIds ID
*/
async deleteUsers(externalIds: string[]): Promise<void> {
async deleteUsers(context: Context, externalIds: string[]): Promise<void> {
this.logger.log(
`[IN]${this.deleteUsers.name} | params: { externalIds: ${externalIds} };`
`[IN] [${context.getTrackingId()}] ${
this.deleteUsers.name
} | params: { externalIds: ${externalIds} };`
);
try {

View File

@ -89,8 +89,10 @@ export class BlobstorageService {
*
* @returns containers
*/
async deleteContainers(): Promise<void> {
this.logger.log(`[IN] ${this.deleteContainers.name}`);
async deleteContainers(context: Context): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.deleteContainers.name}`
);
try {
for await (const container of this.blobServiceClientAU.listContainers({

View File

@ -1,20 +1,18 @@
import { Injectable } from '@nestjs/common';
import {
DataSource,
} from 'typeorm';
import { User } from '../users/entity/user.entity';
import { Account } from './entity/account.entity';
import { Injectable } from "@nestjs/common";
import { DataSource } from "typeorm";
import { User } from "../users/entity/user.entity";
import { Account } from "./entity/account.entity";
import {
getDirection,
getTaskListSortableAttribute,
} from '../../common/types/sort/util';
} from "../../common/types/sort/util";
import { SortCriteria } from "../sort_criteria/entity/sort_criteria.entity";
import {
insertEntity,
updateEntity,
deleteEntity,
} from '../../common/repository';
import { Context } from '../../common/log';
} from "../../common/repository";
import { Context } from "../../common/log";
@Injectable()
export class AccountsRepositoryService {
@ -30,6 +28,7 @@ export class AccountsRepositoryService {
* @param tier
* @param adminExternalUserId
* @param adminUserRole
* @param adminUserAuthId
* @param accountId
* @param userId
* @param adminUserAcceptedEulaVersion
@ -45,6 +44,7 @@ export class AccountsRepositoryService {
tier: number,
adminExternalUserId: string,
adminUserRole: string,
adminUserAuthId: string,
accountId: number,
userId: number,
adminUserAcceptedEulaVersion?: string,
@ -77,10 +77,12 @@ 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;
user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null;
user.email_verified = true;
}
const usersRepo = entityManager.getRepository(User);
const newUser = usersRepo.create(user);

View File

@ -1,11 +1,15 @@
import { Injectable } from "@nestjs/common";
import { DataSource } from "typeorm";
import { logger } from "@azure/identity";
import { Account } from "./entity/account.entity";
import { AUTO_INCREMENT_START } from "../../constants";
import { Term } from "./entity/term.entity";
import { insertEntities } from "../../common/repository";
import { Context } from "../../common/log";
@Injectable()
export class DeleteRepositoryService {
// クエリログにコメントを出力するかどうか
private readonly isCommentOut = process.env.STAGE !== "local";
constructor(private dataSource: DataSource) {}
/**
@ -54,4 +58,35 @@ export class DeleteRepositoryService {
await queryRunner.release();
}
}
/**
*
* @returns data
*/
async insertInitData(context: Context): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const termRepo = entityManager.getRepository(Term);
// ワークフローのデータ作成
const newTarmDpa = new Term();
newTarmDpa.document_type = "DPA";
newTarmDpa.version = "V0.1";
const newTarmEula = new Term();
newTarmEula.document_type = "EULA";
newTarmEula.version = "V0.1";
const newTarmPrivacyNotice = new Term();
newTarmPrivacyNotice.document_type = "PrivacyNotice";
newTarmPrivacyNotice.version = "V0.1";
const initTerms = [newTarmDpa, newTarmEula, newTarmPrivacyNotice];
await insertEntities(
Term,
termRepo,
initTerms,
this.isCommentOut,
context
);
});
}
}

View File

@ -129,6 +129,33 @@ export class CardLicense {
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
updated_at: Date;
}
@Entity({ name: "card_license_issue" })
export class CardLicenseIssue {
@PrimaryGeneratedColumn()
id: number;
@Column()
issued_at: Date;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",

View File

@ -2,6 +2,7 @@ import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import {
CardLicense,
CardLicenseIssue,
License,
LicenseAllocationHistory,
} from "./entity/license.entity";
@ -9,7 +10,11 @@ import { LicensesRepositoryService } from "./licenses.repository.service";
@Module({
imports: [
TypeOrmModule.forFeature([License, CardLicense, LicenseAllocationHistory]),
TypeOrmModule.forFeature([
License,
CardLicense,
CardLicenseIssue, LicenseAllocationHistory,
]),
],
providers: [LicensesRepositoryService],
exports: [LicensesRepositoryService],

View File

@ -1,17 +1,17 @@
import { Injectable, Logger } from "@nestjs/common";
import { DataSource, In } from "typeorm";
import { DataSource } from "typeorm";
import {
License,
LicenseAllocationHistory,
CardLicense,
CardLicenseIssue,
} from "./entity/license.entity";
import { insertEntities } from "../../common/repository";
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 { CardLicensesFile, LicensesFile } from "src/common/types/types";
@Injectable()
export class LicensesRepositoryService {
//クエリログにコメントを出力するかどうか
@ -22,25 +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) : null;
if (licensesInputFile.allocated_user_id) {
license.allocated_user_id = licensesInputFile.allocated_user_id;
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 (LicensesFile.allocated_user_id) {
license.allocated_user_id = LicensesFile.allocated_user_id;
}
newLicenses.push(license);
});
@ -89,30 +91,68 @@ 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);
const licensesRepo = entityManager.getRepository(License);
const cardLicenseIssueRepo =
entityManager.getRepository(CardLicenseIssue);
const licenses: License[] = [];
// ライセンステーブルを作成するBULK INSERT)
for (let i = 0; i < cardLicensesFiles.length; i++) {
const license = new License();
license.account_id = AUTO_INCREMENT_START; // 最初に登場するアカウント(第一アカウント)
license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
license.type = LICENSE_TYPE.CARD;
licenses.push(license);
}
const savedLicenses = await insertEntities(
License,
licensesRepo,
licenses,
this.isCommentOut,
context
);
let newCardLicenses: CardLicense[] = [];
cardLicensesInputFiles.forEach((cardLicensesInputFile) => {
// カードライセンス発行テーブルを作成する
const cardLicenseIssue = new CardLicenseIssue();
cardLicenseIssue.issued_at = new Date();
const newCardLicenseIssue = cardLicenseIssueRepo.create(cardLicenseIssue);
const savedCardLicensesIssue = await insertEntity(
CardLicenseIssue,
cardLicenseIssueRepo,
newCardLicenseIssue,
this.isCommentOut,
context
);
const newCardLicenses: CardLicense[] = [];
// カードライセンステーブルを作成するBULK INSERT)
for (let i = 0; i < cardLicensesFiles.length; i++) {
const cardLicense = new CardLicense();
cardLicense.license_id = cardLicensesInputFile.license_id;
cardLicense.issue_id = cardLicensesInputFile.issue_id;
cardLicense.card_license_key = cardLicensesInputFile.card_license_key;
cardLicense.activated_at = (cardLicensesInputFile.activated_at) ? new Date(cardLicensesInputFile.activated_at) : null;
cardLicense.created_at = (cardLicensesInputFile.created_at) ? new Date(cardLicensesInputFile.created_at) : null;
cardLicense.created_by = cardLicensesInputFile.created_by;
cardLicense.updated_at = (cardLicensesInputFile.updated_at) ? new Date(cardLicensesInputFile.updated_at) : null;
cardLicense.updated_by = cardLicensesInputFile.updated_by;
cardLicense.license_id = savedLicenses[i].id; // Licenseテーブルの自動採番されたIDを挿入
cardLicense.issue_id = savedCardLicensesIssue.id; // CardLicenseIssueテーブルの自動採番されたIDを挿入
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 = cardLicensesFiles[i].created_at
? new Date(cardLicensesFiles[i].created_at)
: null;
cardLicense.created_by = cardLicensesFiles[i].created_by;
cardLicense.updated_at = cardLicensesFiles[i].updated_at
? new Date(cardLicensesFiles[i].updated_at)
: null;
cardLicense.updated_by = cardLicensesFiles[i].updated_by;
newCardLicenses.push(cardLicense);
});
}
const query = cardLicenseRepo
.createQueryBuilder()
@ -126,5 +166,4 @@ export class LicensesRepositoryService {
return {};
});
}
}

View File

@ -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<void> {
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({

View File

@ -11,6 +11,7 @@ import {
selectEmail,
selectInputValidationErrors,
selectIsLoading,
selectOffset,
} from "features/partner/selectors";
import {
changeAdminName,
@ -19,7 +20,11 @@ import {
changeEmail,
cleanupAddPartner,
} from "features/partner/partnerSlice";
import { createPartnerAccountAsync } from "features/partner";
import {
LIMIT_PARTNER_VIEW_NUM,
createPartnerAccountAsync,
getPartnerInfoAsync,
} from "features/partner";
import close from "../../assets/images/close.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
import { COUNTRY_LIST } from "../SignupPage/constants";
@ -50,6 +55,7 @@ export const AddPartnerAccountPopup: React.FC<AddPartnerAccountPopup> = (
const adminName = useSelector(selectAdminName);
const email = useSelector(selectEmail);
const isLoading = useSelector(selectIsLoading);
const offset = useSelector(selectOffset);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
@ -84,6 +90,12 @@ export const AddPartnerAccountPopup: React.FC<AddPartnerAccountPopup> = (
setIsPushCreateButton(false);
if (meta.requestStatus === "fulfilled") {
dispatch(
getPartnerInfoAsync({
limit: LIMIT_PARTNER_VIEW_NUM,
offset,
})
);
closePopup();
}
}, [

View File

@ -15,7 +15,7 @@ import { USER_ROLES } from '../../constants';
@ValidatorConstraint({ name: 'IsAuthorId', async: false })
export class IsAuthorId implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
const request = args.object as SignupRequest | PostUpdateUserRequest;
const request = args.object as SignupRequest | PostUpdateUserRequest;
// requestの存在チェック
if (!request) {
return false;

View File

@ -782,6 +782,103 @@ describe('TasksService', () => {
expect(task.optionItemList).toEqual(audioOptionItems);
}
});
it('[Author] Authorは自分が作成者のTask一覧を取得できる(ソート条件がJob_number以外)', async () => {
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();
if (!source) fail();
const module = await makeTaskTestingModuleWithNotificaiton(
source,
notificationhubServiceMockValue,
);
if (!module) fail();
const { id: accountId } = await makeTestSimpleAccount(source);
const { id: userId, external_id } = await makeTestUser(source, {
account_id: accountId,
external_id: 'userId',
role: 'author',
author_id: 'MY_AUTHOR_ID',
});
//「バグ 3661: [FB対応]Option Itemにチェックを付けると真っ白な画面になる」の確認のため
// audio_file_idをTaskIdと異なる値にするために、AudioFileを作成
await createAudioFile(
source,
accountId,
userId,
'MY_AUTHOR_ID',
'',
'00',
);
// Taskを作成
await createTask(
source,
accountId,
userId,
'MY_AUTHOR_ID',
'WORKTYPE1',
'01',
'00000001',
'Uploaded',
);
await createTask(
source,
accountId,
userId,
'MY_AUTHOR_ID',
'WORKTYPE2',
'01',
'00000002',
'Uploaded',
);
const service = module.get<TasksService>(TasksService);
const offset = 0;
const limit = 20;
const status = ['Uploaded', 'Backup'];
// バグ 3786: [FB対応]タスク一覧画面のOptionItemがソート条件によって表示順がおかしくなる の確認のため
// Job_number以外のソート条件を指定
const paramName = 'WORK_TYPE';
const direction = 'DESC';
const { tasks, total } = await service.getTasks(
makeContext('trackingId', 'requestId'),
external_id,
[USER_ROLES.AUTHOR],
offset,
limit,
status,
paramName,
direction,
);
expect(total).toEqual(2);
{
const task = tasks[0];
expect(task.jobNumber).toEqual('00000002');
// ソート条件がJob_number以外でもOptionItemがid順に取得されていることを確認
const audioOptionItems = Array.from({ length: 10 }).map((_, i) => {
return {
optionItemLabel: `label${i}:audio_file_id${task.audioFileId}`,
optionItemValue: `value${i}:audio_file_id${task.audioFileId}`,
};
});
expect(task.optionItemList).toEqual(audioOptionItems);
}
{
const task = tasks[1];
expect(task.jobNumber).toEqual('00000001');
// ソート条件がJob_number以外でもOptionItemがid順に取得されていることを確認
const audioOptionItems = Array.from({ length: 10 }).map((_, i) => {
return {
optionItemLabel: `label${i}:audio_file_id${task.audioFileId}`,
optionItemValue: `value${i}:audio_file_id${task.audioFileId}`,
};
});
expect(task.optionItemList).toEqual(audioOptionItems);
}
});
it('[Author] Authorは同一アカウントであっても自分以外のAuhtorのTaskは取得できない', async () => {
const notificationhubServiceMockValue =
makeDefaultNotificationhubServiceMockValue();

View File

@ -170,7 +170,7 @@ describe('UsersService.confirmUser', () => {
});
expect(_subject).toBe('Account Registered Notification [U-101]');
expect(_url).toBe('http://localhost:8081/');
}, 600000);
});
it('トークンの形式が不正な場合、形式不正エラーとなる。', async () => {
if (!source) fail();
@ -2735,17 +2735,21 @@ describe('UsersService.getRelations', () => {
const worktype1 = await createWorktype(
source,
account.id,
'worktype1',
'worktypeB',
undefined,
true,
);
await createOptionItems(source, worktype1.id);
const worktype2 = await createWorktype(source, account.id, 'worktype2');
const worktype2 = await createWorktype(source, account.id, 'worktypeC');
await createOptionItems(source, worktype2.id);
const worktype3 = await createWorktype(source, account.id, 'worktypeA');
await createOptionItems(source, worktype3.id);
await createWorkflow(source, account.id, user1, worktype1.id);
await createWorkflow(source, account.id, user1, worktype2.id);
await createWorkflow(source, account.id, user1, worktype3.id);
await createWorkflow(source, account.id, user1);
await createWorkflow(source, account.id, user2, worktype1.id);
@ -2754,15 +2758,17 @@ describe('UsersService.getRelations', () => {
const workflows = await getWorkflows(source, account.id);
workflows.sort((a, b) => a.id - b.id);
expect(workflows.length).toBe(4);
expect(workflows.length).toBe(5);
expect(workflows[0].worktype_id).toBe(worktype1.id);
expect(workflows[0].author_id).toBe(user1);
expect(workflows[1].worktype_id).toBe(worktype2.id);
expect(workflows[1].author_id).toBe(user1);
expect(workflows[2].worktype_id).toBe(null);
expect(workflows[2].worktype_id).toBe(worktype3.id);
expect(workflows[2].author_id).toBe(user1);
expect(workflows[3].worktype_id).toBe(worktype1.id);
expect(workflows[3].author_id).toBe(user2);
expect(workflows[3].worktype_id).toBe(null);
expect(workflows[3].author_id).toBe(user1);
expect(workflows[4].worktype_id).toBe(worktype1.id);
expect(workflows[4].author_id).toBe(user2);
}
const context = makeContext(external_id, 'requestId');
@ -2778,14 +2784,17 @@ describe('UsersService.getRelations', () => {
expect(relations.authorIdList[1]).toBe('AUTHOR_2');
const workTypeList = relations.workTypeList;
expect(relations.workTypeList.length).toBe(2);
expect(workTypeList[0].workTypeId).toBe(worktype1.custom_worktype_id);
expect(relations.workTypeList.length).toBe(3);
// Workflowの作成順ではなくcustom_worktype_idの昇順で取得するためWorkTypeListの先頭はworktype3
expect(workTypeList[0].workTypeId).toBe(worktype3.custom_worktype_id);
expect(workTypeList[0].optionItemList.length).toBe(10);
expect(workTypeList[0].optionItemList[0].label).toBe('');
expect(workTypeList[0].optionItemList[0].initialValueType).toBe(2);
expect(workTypeList[0].optionItemList[0].defaultValue).toBe('');
expect(workTypeList[1].workTypeId).toBe(worktype2.custom_worktype_id);
expect(workTypeList[1].workTypeId).toBe(worktype1.custom_worktype_id);
expect(workTypeList[1].optionItemList.length).toBe(10);
expect(workTypeList[2].workTypeId).toBe(worktype2.custom_worktype_id);
expect(workTypeList[2].optionItemList.length).toBe(10);
expect(relations.isEncrypted).toBe(true);
expect(relations.encryptionPassword).toBe('password');

View File

@ -1462,78 +1462,91 @@ 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漏れが発生した場合に型エラーになるようにする

View File

@ -918,6 +918,14 @@ export class UsersRepositoryService {
option_items: true,
},
},
order: {
worktype: {
custom_worktype_id: 'ASC',
option_items: {
id: 'ASC',
},
},
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});

View File

@ -33,30 +33,26 @@
</div>
<div>
<h3>&lt;Deutsch&gt;</h3>
<p>Dear $CUSTOMER_NAME$,</p>
<p>Sehr geehrte(r) $CUSTOMER_NAME$,</p>
<p>
We have received your requested license order.<br />
- Number of licenses ordered: $LICENSE_QUANTITY$<br />
- PO Number: $PO_NUMBER$
Wir haben Ihre gewünschte Lizenzbestellung erhalten.<br />
- Anzahl der bestellten Lizenzen: $LICENSE_QUANTITY$<br />
- Bestellnummer: $PO_NUMBER$
</p>
<p>
Licenses will be issued by your $DEALER_NAME$ which you have selected in
the setting. Licenses issued by your dealer will be stored in your
license inventory. Please log in to the ODMS Cloud to view and assign
licenses to your users.
Die Lizenzen werden von Ihrem $DEALER_NAME$ ausgestellt, den Sie in den Einstellungen ausgewählt haben.
Von Ihrem Händler ausgestellte Lizenzen werden in Ihrem Lizenzbestand gespeichert.
Bitte melden Sie sich bei der ODMS Cloud an, um Lizenzen anzuzeigen und Ihren Benutzern zuzuweisen.
</p>
<p>
Licenses are valid for 12 months from the date they are assigned to a
user.
Lizenzen sind ab dem Datum, an dem sie einem Benutzer zugewiesen wurden, 12 Monate lang gültig.
</p>
<p>
If you need support regarding ODMS Cloud, please contact $DEALER_NAME$.
Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$.
</p>
<p>
If you have received this e-mail in error, please delete this e-mail
from your system.<br />
This is an automatically generated e-mail and this mailbox is not
monitored. Please do not reply.
Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.<br />
Dies ist eine automatisch generierte E-Mail und dieses Postfach wird nicht überwacht. Bitte nicht antworten.
</p>
</div>
<div>

View File

@ -17,20 +17,20 @@ This is an automatically generated e-mail and this mailbox is not monitored. P
<Deutsch>
Dear $CUSTOMER_NAME$,
Sehr geehrte(r) $CUSTOMER_NAME$,
We have received your requested license order.
- Number of licenses ordered: $LICENSE_QUANTITY$
- PO Number: $PO_NUMBER$
Wir haben Ihre gewünschte Lizenzbestellung erhalten.
- Anzahl der bestellten Lizenzen: $LICENSE_QUANTITY$
- Bestellnummer: $PO_NUMBER$
Licenses will be issued by your $DEALER_NAME$ which you have selected in the setting. Licenses issued by your dealer will be stored in your license inventory. Please log in to the ODMS Cloud to view and assign licenses to your users.
Die Lizenzen werden von Ihrem $DEALER_NAME$ ausgestellt, den Sie in den Einstellungen ausgewählt haben. Von Ihrem Händler ausgestellte Lizenzen werden in Ihrem Lizenzbestand gespeichert. Bitte melden Sie sich bei der ODMS Cloud an, um Lizenzen anzuzeigen und Ihren Benutzern zuzuweisen.
Licenses are valid for 12 months from the date they are assigned to a user.
Lizenzen sind ab dem Datum, an dem sie einem Benutzer zugewiesen wurden, 12 Monate lang gültig.
If you need support regarding ODMS Cloud, please contact $DEALER_NAME$.
Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$.
If you have received this e-mail in error, please delete this e-mail from your system.
This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply.
Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
Dies ist eine automatisch generierte E-Mail und dieses Postfach wird nicht überwacht. Bitte nicht antworten.
<Français>