diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 9931cf2..ff4098b 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -3263,7 +3263,7 @@ "description": "セカンダリ管理者ID" } }, - "required": ["delegationPermission"] + "required": ["delegationPermission", "primaryAdminUserId"] }, "UpdateAccountInfoResponse": { "type": "object", "properties": {} }, "ConfirmRequest": { diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index f6b6ff7..c639807 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -36,6 +36,7 @@ export const ErrorCodes = [ 'E010302', // authorId重複エラー 'E010401', // PONumber重複エラー 'E010501', // アカウント不在エラー + 'E010502', // アカウント情報変更不可エラー 'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない) 'E010602', // タスク変更権限不足エラー 'E010603', // タスク不在エラー diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 5ffdfec..484e9a4 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -25,6 +25,7 @@ export const errors: Errors = { E010302: 'This AuthorId already used Error', E010401: 'This PoNumber already used Error', E010501: 'Account not Found Error.', + E010502: 'Account information cannot be changed Error.', E010601: 'Task is not Editable Error', E010602: 'No task edit permissions Error', E010603: 'Task not found Error.', diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 7d6bde3..bda8d6d 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -1002,18 +1002,18 @@ export class AccountsController { secondryAdminUserId, } = body; const token = retrieveAuthorizationToken(req); - const { userId } = jwt.decode(token, { json: true }) as AccessToken; - + const { userId, tier } = jwt.decode(token, { json: true }) as AccessToken; const context = makeContext(userId); - // 仮。API実装で本実装 - // await this.accountService.updateAccountInfo( - // context, - // userId, - // parentAccountId, - // delegationPermission, - // primaryAdminUserId, - // secondryAdminUserId, - // ); + + await this.accountService.updateAccountInfo( + context, + userId, + tier, + delegationPermission, + primaryAdminUserId, + parentAccountId, + secondryAdminUserId, + ); return; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 6f61cf0..6607a87 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -4974,6 +4974,179 @@ describe('パートナー一覧取得', () => { }); }); +describe('アカウント情報更新', () => { + let source: DataSource = null; + beforeEach(async () => { + source = new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: false, + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + await source.destroy(); + source = null; + }); + it('アカウント情報を更新する(第五階層が実行/セカンダリ管理者ユーザがnull)', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( + source, + ); + const tier5Accounts = await makeTestAccount(source, { + parent_account_id: tier4Accounts[0].account.id, + tier: 5, + }); + await service.updateAccountInfo( + makeContext('trackingId'), + tier5Accounts.admin.external_id, + tier5Accounts.account.tier, + true, + tier5Accounts.admin.id, + tier4Accounts[0].account.id, + undefined, + ); + + // DB内が想定通りになっているか確認 + const account = await getAccount(source, tier5Accounts.account.id); + expect(account.parent_account_id).toBe(tier4Accounts[0].account.id); + expect(account.delegation_permission).toBe(true); + expect(account.primary_admin_user_id).toBe(tier5Accounts.admin.id); + expect(account.secondary_admin_user_id).toBe(null); + }); + it('アカウント情報を更新する(第五階層以外が実行)', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const { tier3Accounts: tier3Accounts, tier4Accounts: tier4Accounts } = + await makeHierarchicalAccounts(source); + const adduser = await makeTestUser(source, { + account_id: tier4Accounts[0].account.id, + external_id: 'typist-user-external-id', + role: 'typist', + }); + await service.updateAccountInfo( + makeContext('trackingId'), + tier4Accounts[0].users[0].external_id, + tier4Accounts[0].account.tier, + false, + tier4Accounts[0].users[0].id, + tier3Accounts[0].account.id, + adduser.id, + ); + + // DB内が想定通りになっているか確認 + const account = await getAccount(source, tier4Accounts[0].account.id); + expect(account.parent_account_id).toBe(tier3Accounts[0].account.id); + expect(account.delegation_permission).toBe(false); + expect(account.primary_admin_user_id).toBe(tier4Accounts[0].users[0].id); + expect(account.secondary_admin_user_id).toBe(adduser.id); + }); + it('アカウント情報を更新する(ディーラーアカウントが未入力)', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const { tier3Accounts: tier3Accounts, tier4Accounts: tier4Accounts } = + await makeHierarchicalAccounts(source); + const adduser = await makeTestUser(source, { + account_id: tier4Accounts[0].account.id, + external_id: 'typist-user-external-id', + role: 'typist', + }); + await service.updateAccountInfo( + makeContext('trackingId'), + tier4Accounts[0].users[0].external_id, + tier4Accounts[0].account.tier, + false, + tier4Accounts[0].users[0].id, + undefined, + adduser.id, + ); + + // DB内が想定通りになっているか確認 + const account = await getAccount(source, tier4Accounts[0].account.id); + expect(account.parent_account_id).toBe(null); + expect(account.delegation_permission).toBe(false); + expect(account.primary_admin_user_id).toBe(tier4Accounts[0].users[0].id); + expect(account.secondary_admin_user_id).toBe(adduser.id); + }); + it('アカウント情報の更新に失敗する(ディーラー未存在)', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( + source, + ); + const adduser = await makeTestUser(source, { + account_id: tier4Accounts[0].account.id, + external_id: 'typist-user-external-id', + role: 'typist', + }); + await expect( + service.updateAccountInfo( + makeContext('trackingId'), + tier4Accounts[0].users[0].external_id, + tier4Accounts[0].account.tier, + false, + tier4Accounts[0].users[0].id, + 123, + adduser.id, + ), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010502'), HttpStatus.BAD_REQUEST), + ); + }); + it('アカウント情報の更新に失敗する(プライマリ管理者ユーザ未存在)', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( + source, + ); + const tier5Accounts = await makeTestAccount(source, { + parent_account_id: tier4Accounts[0].account.id, + tier: 5, + }); + await expect( + service.updateAccountInfo( + makeContext('trackingId'), + tier5Accounts.admin.external_id, + tier5Accounts.account.tier, + true, + 999, + tier4Accounts[0].account.id, + tier4Accounts[1].users[0].id, + ), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010502'), HttpStatus.BAD_REQUEST), + ); + }); + it('アカウント情報の更新に失敗する(セカンダリ管理者ユーザ未存在)', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const { tier4Accounts: tier4Accounts } = await makeHierarchicalAccounts( + source, + ); + const tier5Accounts = await makeTestAccount(source, { + parent_account_id: tier4Accounts[0].account.id, + tier: 5, + }); + await expect( + service.updateAccountInfo( + makeContext('trackingId'), + tier5Accounts.admin.external_id, + tier5Accounts.account.tier, + true, + tier4Accounts[0].users[0].id, + tier4Accounts[0].account.id, + 999, + ), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E010502'), HttpStatus.BAD_REQUEST), + ); + }); +}); + describe('getAccountInfo', () => { let source: DataSource = null; beforeEach(async () => { @@ -4991,7 +5164,6 @@ describe('getAccountInfo', () => { await source.destroy(); source = null; }); - it('パラメータのユーザに対応するアカウント情報を取得できる', async () => { const module = await makeTestingModule(source); // 第五階層のアカウント作成 diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index d2465c2..401bae4 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -37,12 +37,15 @@ import { ExpirationThresholdDate, } from '../licenses/types/types'; import { GetLicenseSummaryResponse, Typist } from './types/types'; -import { AccessToken } from '../../common/token'; import { UserNotFoundError } from '../../repositories/users/errors/types'; import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service'; import { makePassword } from '../../common/password'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; -import { AccountNotFoundError } from '../../repositories/accounts/errors/types'; +import { + AccountNotFoundError, + AdminUserNotFoundError, + DealerAccountNotFoundError, +} from '../../repositories/accounts/errors/types'; import { Context } from '../../common/log'; import { LicensesShortageError, @@ -1605,4 +1608,72 @@ export class AccountsService { this.logger.log(`[OUT] [${context.trackingId}] ${this.getPartners.name}`); } } + + /** + * アカウント情報を設定する + * @param context + * @param externalId + * @param tier + * @param delegationPermission + * @param primaryAdminUserId + * @param parentAccountId + * @param secondryAdminUserId + * @returns UpdateAccountInfoResponse + */ + async updateAccountInfo( + context: Context, + externalId: string, + tier: number, + delegationPermission: boolean, + primaryAdminUserId: number, + parentAccountId?: number, + secondryAdminUserId?: number, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.updateAccountInfo.name} | params: { ` + + `externalId: ${externalId}, ` + + `delegationPermission: ${delegationPermission}, ` + + `primaryAdminUserId: ${primaryAdminUserId}, ` + + `parentAccountId: ${parentAccountId}, ` + + `secondryAdminUserId: ${secondryAdminUserId}, };`, + ); + + try { + const { account_id: accountId } = + await this.usersRepository.findUserByExternalId(externalId); + await this.accountRepository.updateAccountInfo( + accountId, + tier, + delegationPermission, + primaryAdminUserId, + parentAccountId, + secondryAdminUserId, + ); + } catch (e) { + this.logger.error(`[${context.trackingId}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case DealerAccountNotFoundError: + case AdminUserNotFoundError: + throw new HttpException( + makeErrorResponse('E010502'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.updateAccountInfo.name}`, + ); + } + } } diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 99672e3..f9675a0 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -547,9 +547,8 @@ export class UpdateAccountInfoRequest { parentAccountId?: number | undefined; @ApiProperty({ description: '代行操作許可' }) delegationPermission: boolean; - @ApiProperty({ description: 'プライマリ管理者ID', required: false }) - @IsOptional() - primaryAdminUserId?: number | undefined; + @ApiProperty({ description: 'プライマリ管理者ID' }) + primaryAdminUserId: number; @ApiProperty({ description: 'セカンダリ管理者ID', required: false }) @IsOptional() secondryAdminUserId?: number | undefined; diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index 6636f21..5da84e7 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -29,7 +29,11 @@ import { PartnerLicenseInfoForRepository, PartnerInfoFromDb, } from '../../features/accounts/types/types'; -import { AccountNotFoundError } from './errors/types'; +import { + AccountNotFoundError, + AdminUserNotFoundError, + DealerAccountNotFoundError, +} from './errors/types'; import { AlreadyLicenseAllocatedError, AlreadyLicenseStatusChangedError, @@ -769,6 +773,103 @@ export class AccountsRepositoryService { } /** + * 一階層上のアカウントを取得する + * @param accountId + * @param tier + * @returns account: 一階層上のアカウント + */ + async getOneUpperTierAccount( + accountId: number, + tier: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountRepo = entityManager.getRepository(Account); + return await accountRepo.findOne({ + where: { + id: accountId, + tier: tier - 1, + }, + }); + }); + } + + /** + * アカウント情報を保存する + * @param myAccountId + * @param tier + * @param delegationPermission + * @param primaryAdminUserId + * @param parentAccountId + * @param secondryAdminUserId + */ + async updateAccountInfo( + myAccountId: number, + tier: number, + delegationPermission: boolean, + primaryAdminUserId: number, + parentAccountId?: number, + secondryAdminUserId?: number, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + // ディーラーアカウントが指定されている場合、存在チェックを行う + if (parentAccountId) { + const dealerAccount = await this.getOneUpperTierAccount( + parentAccountId, + tier, + ); + // 取得できない場合、エラー + if (!dealerAccount) { + throw new DealerAccountNotFoundError( + `Dealer account is not found. id: ${parentAccountId}}`, + ); + } + } + + const userRepo = entityManager.getRepository(User); + // プライマリ管理者ユーザーの存在チェック + if (primaryAdminUserId) { + const primaryAdminUser = await userRepo.findOne({ + where: { + id: primaryAdminUserId, + account_id: myAccountId, + }, + }); + if (!primaryAdminUser) { + throw new AdminUserNotFoundError( + `Primary admin user is not found. id: ${primaryAdminUserId}, account_id: ${myAccountId}`, + ); + } + } + + // セカンダリ管理者ユーザーの存在チェック + if (secondryAdminUserId) { + const secondryAdminUser = await userRepo.findOne({ + where: { + id: secondryAdminUserId, + account_id: myAccountId, + }, + }); + if (!secondryAdminUser) { + throw new AdminUserNotFoundError( + `Secondry admin user is not found. id: ${secondryAdminUserId}, account_id: ${myAccountId}`, + ); + } + } + const accountRepo = entityManager.getRepository(Account); + // アカウント情報を更新 + await accountRepo.update( + { id: myAccountId }, + { + parent_account_id: parentAccountId || null, + delegation_permission: delegationPermission, + primary_admin_user_id: primaryAdminUserId, + secondary_admin_user_id: secondryAdminUserId || null, + }, + ); + }); + } + + /* * ActiveWorktypeIdを更新する * @param accountId * @param [id] ActiveWorktypeIdの内部ID diff --git a/dictation_server/src/repositories/accounts/errors/types.ts b/dictation_server/src/repositories/accounts/errors/types.ts index 9b5145a..c273574 100644 --- a/dictation_server/src/repositories/accounts/errors/types.ts +++ b/dictation_server/src/repositories/accounts/errors/types.ts @@ -1,2 +1,6 @@ // アカウント未発見エラー export class AccountNotFoundError extends Error {} +// ディーラーアカウント未存在エラー +export class DealerAccountNotFoundError extends Error {} +// 管理者ユーザ未存在エラー +export class AdminUserNotFoundError extends Error {}