Merged PR 403: API実装(アカウント設定API)

## 概要
[Task2603: API実装(アカウント設定API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2603)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
getDealerAccountという一階層上のアカウントを取得する共通的なAPIも実装しています。

- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)
なし

## レビューポイント
- 実行中にdealerアカウントに対して変更が走った場合でも対応できるよう、トランザクションをネストした実装にしています。
トランザクションをネストした場合は、内部のトランザクションが正常に完了し、その後外部のトランザクションも正常に完了すると、変更がコミットされます。
- 画面の仕様上、第五階層でないとdealerの変更は行わないが、API側でdelegationPermissionに対する階層(不整合チェック)をやっていないが、問題ないか。
## UIの変更
- Before/Afterのスクショなど
- スクショ置き場
なし

## 動作確認状況
- ローカルで確認
第五階層以外がアカウント情報を設定できる
アカウント情報を更新する(第五階層が実行/セカンダリ管理者ユーザがnull)
・プライマリ管理者ユーザを存在する値にして、更新される
・セカンダリ管理者ユーザをundefinedで入力し、nullで更新される
アカウント情報を更新する(第五階層以外が実行)
アカウント情報を更新する(ディーラーアカウントが未入力)
・parentAccountIdがnullで更新される
アカウント情報の更新に失敗する(ディーラー未存在)
アカウント情報の更新に失敗する(プライマリ管理者ユーザ未存在)
アカウント情報の更新に失敗する(プライマリ管理者ユーザがnull)
アカウント情報の更新に失敗する(セカンダリ管理者ユーザ未存在)
アカウント情報の更新に失敗する(プライマリ管理者ユーザ、セカンダリ管理者ユーザ両方が未入力)
以下POSTMANで確認
プライマリ管理者ユーザIDをundefinedで入力した場合はエラー
管理者権限のないアカウントで実行した場合、権限エラー
500エラー
## 補足
- 相談、参考資料などがあれば
This commit is contained in:
maruyama.t 2023-09-19 07:12:58 +00:00
parent d1a8b887e5
commit 3f5f75a48f
9 changed files with 368 additions and 19 deletions

View File

@ -3263,7 +3263,7 @@
"description": "セカンダリ管理者ID"
}
},
"required": ["delegationPermission"]
"required": ["delegationPermission", "primaryAdminUserId"]
},
"UpdateAccountInfoResponse": { "type": "object", "properties": {} },
"ConfirmRequest": {

View File

@ -36,6 +36,7 @@ export const ErrorCodes = [
'E010302', // authorId重複エラー
'E010401', // PONumber重複エラー
'E010501', // アカウント不在エラー
'E010502', // アカウント情報変更不可エラー
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
'E010602', // タスク変更権限不足エラー
'E010603', // タスク不在エラー

View File

@ -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.',

View File

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

View File

@ -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>(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>(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>(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>(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>(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>(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);
// 第五階層のアカウント作成

View File

@ -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<void> {
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}`,
);
}
}
}

View File

@ -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;

View File

@ -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<Account | undefined> {
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<void> {
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

View File

@ -1,2 +1,6 @@
// アカウント未発見エラー
export class AccountNotFoundError extends Error {}
// ディーラーアカウント未存在エラー
export class DealerAccountNotFoundError extends Error {}
// 管理者ユーザ未存在エラー
export class AdminUserNotFoundError extends Error {}