diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index 7525fb7..f35fe54 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -167,9 +167,7 @@ import { CheckHeaderMiddleware } from './common/check-header.middleware'; }) export class AppModule { configure(consumer: MiddlewareConsumer) { - consumer - .apply(LoggerMiddleware) - .forRoutes(''); + consumer.apply(LoggerMiddleware).forRoutes(''); // stage=localの場合はmiddlewareを適用しない // ローカル環境ではサーバーから静的ファイルも返すため、APIリクエスト以外のリクエストにもmiddlewareが適用されてしまう if (process.env.STAGE !== 'local') { diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 29f63b5..0e56a73 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -2393,13 +2393,8 @@ export class AccountsController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - // TODO:service層を呼び出す。本実装時に以下は削除する。 const { to, children } = body; - this.logger.log( - `[${context.getTrackingId()}] to : ${to}, children : ${children.join( - ', ', - )}`, - ); + await this.accountService.switchParent(context, to, children); return {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index c20e6e6..7b43645 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -19,6 +19,7 @@ import { createLicenseSetExpiryDateAndStatus, createOptionItems, createWorktype, + getLicenseOrders, getOptionItems, getSortCriteria, getTypistGroup, @@ -92,6 +93,7 @@ import { truncateAllTable } from '../../common/test/init'; import { createTask, getCheckoutPermissions } from '../tasks/test/utility'; import { createCheckoutPermissions } from '../tasks/test/utility'; import { TestLogger } from '../../common/test/logger'; +import { Account } from '../../repositories/accounts/entity/account.entity'; describe('createAccount', () => { let source: DataSource | null = null; @@ -7930,3 +7932,383 @@ describe('updateRestrictionStatus', () => { } }); }); + +describe('switchParent', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('第三階層<->第四階層間の階層構造変更処理ができる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: newParent } = await makeTestAccount(source, { + tier: 3, + country: `AU`, + }); + + // 子アカウントを作成する + const { account: child1 } = await makeTestAccount(source, { + tier: 4, + country: `AU`, + parent_account_id: undefined, + delegation_permission: true, + }); + + const { account: child2 } = await makeTestAccount(source, { + tier: 4, + country: `NZ`, // 同じリージョンで切り替えできることの確認 + parent_account_id: undefined, + delegation_permission: true, + }); + + // ライセンス注文作成 + await createLicenseOrder(source, child1.id, 10, 1); // 注文先アカウントは何でもいいため適当 + await createLicenseOrder(source, child1.id, 10, 1); // 親アカウントは何でもいいため適当 + await createLicenseOrder(source, child2.id, 10, 1); // 親アカウントは何でもいいため適当 + + // テスト実行 + const context = makeContext(`external_id`, 'requestId'); + const service = module.get(AccountsService); + await service.switchParent(context, newParent.id, [child1.id, child2.id]); + + const child1Result = await getAccount(source, child1.id); + const child2Result = await getAccount(source, child2.id); + const child1LicenseOrderResult = await getLicenseOrders(source, child1.id); + const child2LicenseOrderResult = await getLicenseOrders(source, child2.id); + + // アカウントテーブルの更新確認 + expect(child1Result?.parent_account_id).toBe(newParent.id); + expect(child1Result?.delegation_permission).toBe(false); + expect(child2Result?.parent_account_id).toBe(newParent.id); + expect(child2Result?.delegation_permission).toBe(false); + + // ライセンス注文が全てcancelされていることの確認 + expect(child1LicenseOrderResult.length).toBe(2); + const child1LicenseOrderStatuses = child1LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child1LicenseOrderStatuses).toBeTruthy(); + expect(child2LicenseOrderResult.length).toBe(1); + const child2LicenseOrderStatuses = child2LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child2LicenseOrderStatuses).toBeTruthy(); + }); + + it('第四階層<->第五階層間の階層構造変更処理ができる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: newParent } = await makeTestAccount(source, { + tier: 4, + country: `AU`, + }); + + // 子アカウントを作成する + const { account: child1 } = await makeTestAccount(source, { + tier: 5, + country: `AU`, + parent_account_id: undefined, + delegation_permission: true, + }); + + const { account: child2 } = await makeTestAccount(source, { + tier: 5, + country: `AU`, + parent_account_id: undefined, + delegation_permission: true, + }); + + // ライセンス注文作成 + await createLicenseOrder(source, child1.id, 10, 1); // 注文先アカウントは何でもいいため適当 + await createLicenseOrder(source, child1.id, 10, 1); // 親アカウントは何でもいいため適当 + await createLicenseOrder(source, child2.id, 10, 1); // 親アカウントは何でもいいため適当 + + // テスト実行 + const context = makeContext(`external_id`, 'requestId'); + const service = module.get(AccountsService); + await service.switchParent(context, newParent.id, [child1.id, child2.id]); + + const child1Result = await getAccount(source, child1.id); + const child2Result = await getAccount(source, child2.id); + const child1LicenseOrderResult = await getLicenseOrders(source, child1.id); + const child2LicenseOrderResult = await getLicenseOrders(source, child2.id); + + // アカウントテーブルの更新確認 + expect(child1Result?.parent_account_id).toBe(newParent.id); + expect(child1Result?.delegation_permission).toBe(false); + expect(child2Result?.parent_account_id).toBe(newParent.id); + expect(child2Result?.delegation_permission).toBe(false); + + // ライセンス注文が全てcancelされていることの確認 + expect(child1LicenseOrderResult.length).toBe(2); + const child1LicenseOrderStatuses = child1LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child1LicenseOrderStatuses).toBeTruthy(); + expect(child2LicenseOrderResult.length).toBe(1); + const child2LicenseOrderStatuses = child2LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child2LicenseOrderStatuses).toBeTruthy(); + }); + + it('切り替え先親アカウントが存在しない場合は400エラー(親アカウント不在エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // 子アカウントの取得では1件だけ返すようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child = new Account(); + child.id = 1; + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + try { + // 切り替え先アカウントを作成せずに実行する + await service.switchParent(context, 10, [child.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017001')); + } else { + fail(); + } + } + }); + + it('切り替え先親アカウントが第三・第四以外の階層の場合は400エラー(階層関係不適切エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // 子アカウントの取得では1件だけ返すようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child = new Account(); + child.id = 1; + child.tier = 4; + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + + // 親アカウントの階層が第五階層の場合に失敗する + const parent = new Account(); + parent.id = 10; + try { + parent.tier = 5; + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017002')); + } else { + fail(); + } + } + + try { + // 親アカウントの階層が第二階層の場合に失敗する + parent.tier = 2; + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017002')); + } else { + fail(); + } + } + + try { + // 親アカウントの階層が第一階層の場合に失敗する + parent.tier = 1; + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017002')); + } else { + fail(); + } + } + }); + + it('第五階層の子アカウントに対して、第三階層の切り替え先親アカウントを指定した場合は400エラー(階層関係不適切エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // 子アカウントの取得では1件だけ返すようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child1 = new Account(); + child1.id = 1; + child1.tier = 5; + const child2 = new Account(); + child1.id = 1; + child1.tier = 4; + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child1, child2]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + + const parent = new Account(); + parent.id = 10; + parent.tier = 3; + try { + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child1.id, child2.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017002')); + } else { + fail(); + } + } + }); + + it('第三<->第四の切り替えで、親子でリージョンが異なる場合は400エラー(リージョン関係不一致エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child1 = new Account(); + child1.id = 1; + child1.tier = 4; + child1.country = 'AU'; + + const child2 = new Account(); + child2.id = 2; + child2.tier = 4; + child2.country = 'US'; // このアカウントだけリージョンが異なるようにしておく + + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child1, child2]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + + const parent = new Account(); + parent.id = 10; + parent.tier = 3; + parent.country = 'AU'; + try { + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child1.id, child2.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017003')); + } else { + fail(); + } + } + }); + + it('第四<->第五の切り替えで、親子で国が異なる場合は400エラー(国関係不一致エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child1 = new Account(); + child1.id = 1; + child1.tier = 5; + child1.country = 'AU'; + + const child2 = new Account(); + child2.id = 2; + child2.tier = 5; + child2.country = 'NZ'; // このアカウントだけ国が異なるようにしておく + + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child1, child2]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + + const parent = new Account(); + parent.id = 10; + parent.tier = 4; + parent.country = 'AU'; + try { + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child1.id, child2.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017004')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 1cd288f..d6af114 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -15,6 +15,9 @@ import { OPTION_ITEM_VALUE_TYPE, MANUAL_RECOVERY_REQUIRED, LICENSE_ISSUE_STATUS, + BLOB_STORAGE_REGION_AU, + BLOB_STORAGE_REGION_EU, + BLOB_STORAGE_REGION_US, } from '../../constants'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { @@ -47,7 +50,10 @@ import { LicensesRepositoryService } from '../../repositories/licenses/licenses. import { AccountNotFoundError, AdminUserNotFoundError, + CountryMismatchError, DealerAccountNotFoundError, + HierarchyMismatchError, + RegionMismatchError, } from '../../repositories/accounts/errors/types'; import { Context } from '../../common/log'; import { @@ -2633,4 +2639,198 @@ export class AccountsService { ); } } + + /** + * 指定した子アカウントの親アカウントを、指定した親アカウントに変更する + * @param context + * @param newParent + * @param children + * @returns parent + */ + async switchParent( + context: Context, + newParent: number, + children: number[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.switchParent.name + } | params: { ` + + `newParent: ${newParent}, ` + + `children: ${children.join(', ')}};`, + ); + + try { + // 切り替え対象の情報取得 + const childrenAccounts = await this.accountRepository.findAccountsById( + context, + children, + ); + if (childrenAccounts.length !== children.length) { + // 指定された子アカウントが一つでも存在しない場合は通常運用ではありえないので汎用エラー + throw new Error('Some children accounts are not found'); + } + + const parentAccount = await this.accountRepository.findAccountById( + context, + newParent, + ); + if (!parentAccount) { + // 指定された親アカウントが存在しない場合は通常運用で起こりうるため、BAD_REQUEST + throw new AccountNotFoundError( + `Parent account is not found. accountId=${newParent}`, + ); + } + + // 切り替え可否チェック(階層関係) + if ( + !this.isValidHierarchyRelation( + parentAccount.tier, + childrenAccounts.map((x) => x.tier), + ) + ) { + throw new HierarchyMismatchError( + `Invalid hierarchy relation. parentAccount.tier=${parentAccount.tier}`, + ); + } + // 切り替え可否チェック(リージョン・国関係) + const { success, errorType } = this.isValidLocationRelation( + parentAccount, + childrenAccounts, + ); + if (!success) { + throw errorType; + } + + // 切り替え処理実施 + await this.accountRepository.switchParentAccount( + context, + newParent, + children, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case AccountNotFoundError: + throw new HttpException( + makeErrorResponse('E017001'), + HttpStatus.BAD_REQUEST, + ); + case HierarchyMismatchError: + throw new HttpException( + makeErrorResponse('E017002'), + HttpStatus.BAD_REQUEST, + ); + case RegionMismatchError: + throw new HttpException( + makeErrorResponse('E017003'), + HttpStatus.BAD_REQUEST, + ); + case CountryMismatchError: + throw new HttpException( + makeErrorResponse('E017004'), + HttpStatus.BAD_REQUEST, + ); + } + } + + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.switchParent.name}`, + ); + } + } + + /** + * 切り替え対象の親アカウントと子アカウントの階層関係が正しいかどうかをチェックする + * @param parentTier + * @param childrenTiers + * @returns true if valid hierarchy relation + */ + private isValidHierarchyRelation( + parentTier: number, + childrenTiers: number[], + ): boolean { + // 全ての子アカウントの階層が、親アカウントの階層の一つ下であるかつ、第三<->第四または第四<->第五の切り替えの場合のみ判定OK。 + if ( + parentTier === TIERS.TIER3 && + childrenTiers.every((child) => child === TIERS.TIER4) + ) { + return true; + } else if ( + parentTier === TIERS.TIER4 && + childrenTiers.every((child) => child === TIERS.TIER5) + ) { + return true; + } + + return false; + } + + /** + * 切り替え対象の親アカウントと子アカウントのリージョン・国関係が正しいかどうかをチェックする。 + * @param parent + * @param children + * @returns valid location relation + */ + private isValidLocationRelation( + parent: Account, + children: Account[], + ): { + success: boolean; + errorType: null | RegionMismatchError | CountryMismatchError; + } { + // 第三<->第四の切り替えはリージョンの一致を確認し、第四<->第五の切り替えは国の一致を確認する。 + if (parent.tier === TIERS.TIER3) { + if ( + !children.every( + (child) => + this.getRegion(child.country) === this.getRegion(parent.country), + ) + ) { + return { + success: false, + errorType: new RegionMismatchError('Invalid region relation'), + }; + } + + return { success: true, errorType: null }; + } else if (parent.tier === TIERS.TIER4) { + if (!children.every((child) => child.country === parent.country)) { + return { + success: false, + errorType: new CountryMismatchError('Invalid country relation'), + }; + } + + return { success: true, errorType: null }; + } else { + // 親アカウントの階層が想定外の場合、本関数の使い方が間違っているので例外を投げる + throw new Error('Not implemented'); + } + } + + /** + * 国の所属する地域を取得する。 + * @param country + * @returns region + */ + private getRegion(country: string): string { + // OMDS様より、地域はBlobStorageのリージョンで判定するでOKとのこと。 + if (BLOB_STORAGE_REGION_AU.includes(country)) { + return 'AU'; + } else if (BLOB_STORAGE_REGION_EU.includes(country)) { + return 'EU'; + } else if (BLOB_STORAGE_REGION_US.includes(country)) { + return 'US'; + } else { + // ここに到達する場合は、国が想定外の値であるため、例外を投げる + throw new Error(`Invalid country. country=${country}`); + } + } } diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index ff0e436..046c2d2 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -248,3 +248,15 @@ export const createAudioFile = async ( const audioFile = audioFileIdentifiers.pop() as AudioFile; return { audioFileId: audioFile.id }; }; + +// ライセンス注文を取得する +export const getLicenseOrders = async ( + datasource: DataSource, + accountId: number, +): Promise => { + return await datasource.getRepository(LicenseOrder).find({ + where: { + from_account_id: accountId, + }, + }); +}; diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index f15a428..c063325 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -280,6 +280,23 @@ export class AccountsRepositoryService { return account; } + /** + * 指定されたアカウントIDのアカウント一覧を取得する + * @param context + * @param ids + * @returns accounts by id + */ + async findAccountsById(context: Context, ids: number[]): Promise { + const accounts = await this.dataSource.getRepository(Account).find({ + where: { + id: In(ids), + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + return accounts; + } + /** * OMDSTokyoのアカウント情報を取得する * @param id @@ -1476,4 +1493,58 @@ export class AccountsRepositoryService { ); }); } + + /** + * アカウント階層構造変更とそれに伴う処理を行う。 + * @param context + * @param newParent + * @param children + * @returns parent account + */ + async switchParentAccount( + context: Context, + newParent: number, + children: number[], + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountRepo = entityManager.getRepository(Account); + const childrenAccounts = await accountRepo.find({ + where: { + id: In(children), + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + await updateEntity( + accountRepo, + { id: In(childrenAccounts.map((child) => child.id)) }, + { + parent_account_id: newParent, + delegation_permission: false, + }, + this.isCommentOut, + context, + ); + + const licenseOrderRepo = entityManager.getRepository(LicenseOrder); + const cancelTargets = await licenseOrderRepo.find({ + where: { + from_account_id: In(children), + status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + await updateEntity( + licenseOrderRepo, + { id: In(cancelTargets.map((cancelTarget) => cancelTarget.id)) }, + { + status: LICENSE_ISSUE_STATUS.CANCELED, + canceled_at: new Date(), + }, + this.isCommentOut, + context, + ); + }); + } } diff --git a/dictation_server/src/repositories/accounts/errors/types.ts b/dictation_server/src/repositories/accounts/errors/types.ts index 826700c..4cb5779 100644 --- a/dictation_server/src/repositories/accounts/errors/types.ts +++ b/dictation_server/src/repositories/accounts/errors/types.ts @@ -26,3 +26,31 @@ export class AccountLockedError extends Error { this.name = 'AccountLockedError'; } } + +/** + * 階層構造関係不適切エラー + */ +export class HierarchyMismatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'HierarchyMismatchError'; + } +} +/** + * 所属リージョン不一致エラー + */ +export class RegionMismatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'RegionMismatchError'; + } +} +/** + * 所属国不一致エラー + */ +export class CountryMismatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'CountryMismatchError'; + } +}