Merged PR 383: API実装(パートナー取得API)
## 概要 [Task2540: API実装(パートナー取得API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2540) パートナー取得APIを実装しました。 ## レビューポイント ・データ取得方法が適切かどうか。 以下の優先順位を意識して作成したが適切か?また、意識できていない実装になっていないか? ①QueryBuilderを使用せずに処理する ②RDB、adb2cへのアクセス回数を最小限にする ## UIの変更 なし ## 動作確認状況 ローカルで動作確認済み、UT実施済み ## 補足 プライマリ、セカンダリ管理者IDがない場合のテストはUTでは実装せず、ローカルでの動作確認で正常に動作することを確認しました。 (プライマリ、セカンダリ管理者IDを指定してアカウントを作成するテストユーティリティを作成する必要があるが、あまり汎用的には思えず作成する手間が惜しかったため)
This commit is contained in:
parent
82fb224d67
commit
2812bc3d20
@ -9,6 +9,7 @@ import { UsersRepositoryService } from '../../repositories/users/users.repositor
|
||||
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
|
||||
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
|
||||
import { Account } from '../../repositories/accounts/entity/account.entity';
|
||||
import { AdB2cUser } from '../../gateways/adb2c/types/types';
|
||||
|
||||
// ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ###
|
||||
|
||||
@ -28,6 +29,10 @@ export const overrideAdB2cService = <TService>(
|
||||
username: string,
|
||||
) => Promise<{ sub: string } | ConflictError>;
|
||||
deleteUser?: (externalId: string, context: Context) => Promise<void>;
|
||||
getUsers?: (
|
||||
context: Context,
|
||||
externalIds: string[],
|
||||
) => Promise<AdB2cUser[]>;
|
||||
},
|
||||
): void => {
|
||||
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
|
||||
@ -44,6 +49,12 @@ export const overrideAdB2cService = <TService>(
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (overrides.getUsers) {
|
||||
Object.defineProperty(obj, obj.getUsers.name, {
|
||||
value: overrides.getUsers,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -149,6 +149,8 @@ export const makeTestAccount = async (
|
||||
datasource: DataSource,
|
||||
defaultAccountValue?: AccountDefault,
|
||||
defaultAdminUserValue?: UserDefault,
|
||||
isPrimaryAdminNotExist?: boolean,
|
||||
isSecondaryAdminNotExist?: boolean,
|
||||
): Promise<{ account: Account; admin: User }> => {
|
||||
let accountId: number;
|
||||
let userId: number;
|
||||
@ -198,10 +200,15 @@ export const makeTestAccount = async (
|
||||
}
|
||||
|
||||
// Accountの管理者を設定する
|
||||
let secondaryAdminUserId = null;
|
||||
if (isPrimaryAdminNotExist && !isSecondaryAdminNotExist) {
|
||||
secondaryAdminUserId = userId;
|
||||
}
|
||||
await datasource.getRepository(Account).update(
|
||||
{ id: accountId },
|
||||
{
|
||||
primary_admin_user_id: userId,
|
||||
primary_admin_user_id: isPrimaryAdminNotExist ? null : userId,
|
||||
secondary_admin_user_id: secondaryAdminUserId,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -238,3 +238,11 @@ export const OPTION_ITEM_VALUE_TYPE = {
|
||||
BLANK: 'Blank',
|
||||
LAST_INPUT: 'LastInput',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* ADB2Cユーザのidentity.signInType
|
||||
* @const {string[]}
|
||||
*/
|
||||
export const ADB2C_SIGN_IN_TYPE = {
|
||||
EAMILADDRESS: 'emailAddress',
|
||||
} as const;
|
||||
|
||||
@ -957,45 +957,13 @@ export class AccountsController {
|
||||
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
|
||||
|
||||
const context = makeContext(userId);
|
||||
// TODO: パートナー取得APIで実装
|
||||
// await this.accountService.getPartners(
|
||||
// context,
|
||||
// body.limit,
|
||||
// body.offset,
|
||||
// );
|
||||
const response = await this.accountService.getPartners(
|
||||
context,
|
||||
userId,
|
||||
limit,
|
||||
offset,
|
||||
);
|
||||
|
||||
// 仮のreturn
|
||||
return {
|
||||
total: 1,
|
||||
partners: [
|
||||
{
|
||||
name: 'testA',
|
||||
tier: 5,
|
||||
accountId: 1,
|
||||
country: 'US',
|
||||
primaryAdmin: 'nameA',
|
||||
email: 'aaa@example.com',
|
||||
dealerManagement: true,
|
||||
},
|
||||
{
|
||||
name: 'testB',
|
||||
tier: 5,
|
||||
accountId: 2,
|
||||
country: 'US',
|
||||
primaryAdmin: 'nameB',
|
||||
email: 'bbb@example.com',
|
||||
dealerManagement: false,
|
||||
},
|
||||
{
|
||||
name: 'testC',
|
||||
tier: 5,
|
||||
accountId: 1,
|
||||
country: 'US',
|
||||
primaryAdmin: 'nothing',
|
||||
email: 'nothing',
|
||||
dealerManagement: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,7 @@ import {
|
||||
import { AccountsService } from './accounts.service';
|
||||
import { Context, makeContext } from '../../common/log';
|
||||
import {
|
||||
ADB2C_SIGN_IN_TYPE,
|
||||
LICENSE_ALLOCATED_STATUS,
|
||||
LICENSE_ISSUE_STATUS,
|
||||
LICENSE_TYPE,
|
||||
@ -61,6 +62,7 @@ import {
|
||||
selectOrderLicense,
|
||||
} from '../licenses/test/utility';
|
||||
import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service';
|
||||
import { AdB2cUser } from '../../gateways/adb2c/types/types';
|
||||
import { Worktype } from '../../repositories/worktypes/entity/worktype.entity';
|
||||
|
||||
describe('createAccount', () => {
|
||||
@ -4155,3 +4157,144 @@ 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('パートナー一覧を取得する', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
const service = module.get<AccountsService>(AccountsService);
|
||||
const { tier1Accounts: tier1Accounts, tier2Accounts: tier2Accounts } =
|
||||
await makeHierarchicalAccounts(source);
|
||||
const tier1Difference = await makeTestAccount(source, {
|
||||
tier: 1,
|
||||
});
|
||||
const tier2_3 = await makeTestAccount(
|
||||
source,
|
||||
{
|
||||
parent_account_id: tier1Accounts[0].account.id,
|
||||
tier: 2,
|
||||
},
|
||||
{},
|
||||
true,
|
||||
false,
|
||||
);
|
||||
const tier2_4 = await makeTestAccount(
|
||||
source,
|
||||
{
|
||||
parent_account_id: tier1Accounts[0].account.id,
|
||||
tier: 2,
|
||||
},
|
||||
{},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
await makeTestAccount(source, {
|
||||
parent_account_id: tier1Difference.account.id,
|
||||
tier: 2,
|
||||
});
|
||||
|
||||
const adb2cReturn = [
|
||||
{
|
||||
id: tier2Accounts[0].users[0].external_id,
|
||||
displayName: 'partner1',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'partner1@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: tier2Accounts[1].users[0].external_id,
|
||||
displayName: 'partner2',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'partner2@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as AdB2cUser[];
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
getUsers: async (_context: Context, _externalIds: string[]) => {
|
||||
return adb2cReturn;
|
||||
},
|
||||
});
|
||||
|
||||
const partners = await service.getPartners(
|
||||
makeContext('trackingId'),
|
||||
tier1Accounts[0].users[0].external_id,
|
||||
15,
|
||||
0,
|
||||
);
|
||||
|
||||
// 違うアカウントのパートナーは取得していないこと
|
||||
expect(partners.total).toBe(4);
|
||||
// 会社名の昇順に取得できていること
|
||||
expect(partners.partners[0].name).toBe(
|
||||
tier2Accounts[1].account.company_name,
|
||||
);
|
||||
expect(partners.partners[1].name).toBe(
|
||||
tier2Accounts[0].account.company_name,
|
||||
);
|
||||
expect(partners.partners[2].name).toBe(tier2_3.account.company_name);
|
||||
expect(partners.partners[3].name).toBe(tier2_4.account.company_name);
|
||||
expect(partners.partners[0].email).toBe('partner2@example.com');
|
||||
expect(partners.partners[1].email).toBe('partner1@example.com');
|
||||
expect(partners.partners[2].email).toBeUndefined;
|
||||
expect(partners.partners[3].email).toBeUndefined;
|
||||
|
||||
expect(partners.partners[0].tier).toBe(tier2Accounts[1].account.tier);
|
||||
expect(partners.partners[0].country).toBe(tier2Accounts[1].account.country);
|
||||
expect(partners.partners[0].accountId).toBe(tier2Accounts[1].account.id);
|
||||
expect(partners.partners[0].tier).toBe(tier2Accounts[1].account.tier);
|
||||
expect(partners.partners[0].primaryAdmin).toBe('partner2');
|
||||
expect(partners.partners[0].dealerManagement).toBe(
|
||||
tier2Accounts[1].account.delegation_permission,
|
||||
);
|
||||
});
|
||||
it('パートナー一覧を取得する(パートナーが0件の場合)', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
const service = module.get<AccountsService>(AccountsService);
|
||||
const account = await makeTestAccount(source, {
|
||||
tier: 1,
|
||||
});
|
||||
|
||||
const adb2cReturn = [{}] as AdB2cUser[];
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
getUsers: async (_context: Context, _externalIds: string[]) => {
|
||||
return adb2cReturn;
|
||||
},
|
||||
});
|
||||
|
||||
const partners = await service.getPartners(
|
||||
makeContext('trackingId'),
|
||||
account.admin.external_id,
|
||||
15,
|
||||
0,
|
||||
);
|
||||
|
||||
// 結果が0件で成功となること
|
||||
expect(partners.total).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
} from '../../gateways/adb2c/adb2c.service';
|
||||
import { Account } from '../../repositories/accounts/entity/account.entity';
|
||||
import { User } from '../../repositories/users/entity/user.entity';
|
||||
import { TIERS, USER_ROLES } from '../../constants';
|
||||
import { TIERS, USER_ROLES, ADB2C_SIGN_IN_TYPE } from '../../constants';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import {
|
||||
TypistGroup,
|
||||
@ -23,6 +23,7 @@ import {
|
||||
GetMyAccountResponse,
|
||||
GetTypistGroupResponse,
|
||||
GetWorktypesResponse,
|
||||
GetPartnersResponse,
|
||||
} from './types/types';
|
||||
import {
|
||||
DateWithZeroTime,
|
||||
@ -1312,4 +1313,85 @@ export class AccountsService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* パートナー一覧を取得します
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param limit
|
||||
* @param offset
|
||||
* @returns GetPartnersResponse
|
||||
*/
|
||||
async getPartners(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
limit: number,
|
||||
offset: number,
|
||||
): Promise<GetPartnersResponse> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.getPartners.name} | params: { ` +
|
||||
`externalId: ${externalId}, ` +
|
||||
`limit: ${limit}, ` +
|
||||
`offset: ${offset}, };`,
|
||||
);
|
||||
|
||||
try {
|
||||
const { account_id: accountId } =
|
||||
await this.usersRepository.findUserByExternalId(externalId);
|
||||
|
||||
const partners = await this.accountRepository.getPartners(
|
||||
accountId,
|
||||
limit,
|
||||
offset,
|
||||
);
|
||||
|
||||
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
|
||||
let externalIds = partners.partnersInfo.map(
|
||||
(x) => x.primaryAccountExternalId,
|
||||
);
|
||||
externalIds = externalIds.filter((item) => item !== undefined);
|
||||
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
|
||||
|
||||
// DBから取得した情報とADB2Cから取得した情報をマージ
|
||||
const response = partners.partnersInfo.map((db) => {
|
||||
const adb2cUser = adb2cUsers.find(
|
||||
(adb2c) => db.primaryAccountExternalId === adb2c.id,
|
||||
);
|
||||
|
||||
let primaryAdmin = undefined;
|
||||
let mail = undefined;
|
||||
if (adb2cUser) {
|
||||
primaryAdmin = adb2cUser.displayName;
|
||||
mail = adb2cUser.identities.find(
|
||||
(identity) =>
|
||||
identity.signInType === ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
|
||||
).issuerAssignedId;
|
||||
}
|
||||
return {
|
||||
name: db.name,
|
||||
tier: db.tier,
|
||||
accountId: db.accountId,
|
||||
country: db.country,
|
||||
primaryAdmin: primaryAdmin,
|
||||
email: mail,
|
||||
dealerManagement: db.dealerManagement,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
total: partners.total,
|
||||
partners: response,
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.getPartners.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -480,3 +480,13 @@ export class GetPartnersResponse {
|
||||
@ApiProperty({ type: [Partner] })
|
||||
partners: Partner[];
|
||||
}
|
||||
|
||||
// RepositoryからPartnerLicenseInfoに関する情報を取得する際の型
|
||||
export type PartnerInfoFromDb = {
|
||||
name: string;
|
||||
tier: number;
|
||||
accountId: number;
|
||||
country: string;
|
||||
primaryAccountExternalId: string;
|
||||
dealerManagement: boolean;
|
||||
};
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
TaskListSortableAttribute,
|
||||
} from '../../../common/types/sort';
|
||||
import { AdB2cUser } from '../../../gateways/adb2c/types/types';
|
||||
import { ADB2C_SIGN_IN_TYPE } from '../../../constants';
|
||||
|
||||
export type SortCriteriaRepositoryMockValue = {
|
||||
updateSortCriteria: SortCriteria | Error;
|
||||
@ -403,7 +404,7 @@ const AdB2cMockUsers: AdB2cUser[] = [
|
||||
displayName: 'test1',
|
||||
identities: [
|
||||
{
|
||||
signInType: 'emailAddress',
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'test1@mail.com',
|
||||
},
|
||||
@ -414,7 +415,7 @@ const AdB2cMockUsers: AdB2cUser[] = [
|
||||
displayName: 'test2',
|
||||
identities: [
|
||||
{
|
||||
signInType: 'emailAddress',
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'test2@mail.com',
|
||||
},
|
||||
@ -425,7 +426,7 @@ const AdB2cMockUsers: AdB2cUser[] = [
|
||||
displayName: 'test3',
|
||||
identities: [
|
||||
{
|
||||
signInType: 'emailAddress',
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'test3@mail.com',
|
||||
},
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
UserNotFoundError,
|
||||
} from '../../repositories/users/errors/types';
|
||||
import {
|
||||
ADB2C_SIGN_IN_TYPE,
|
||||
LICENSE_EXPIRATION_THRESHOLD_DAYS,
|
||||
USER_LICENSE_STATUS,
|
||||
USER_ROLES,
|
||||
@ -470,7 +471,7 @@ export class UsersService {
|
||||
|
||||
// メールアドレスを取得する
|
||||
const mail = adb2cUser.identities.find(
|
||||
(identity) => identity.signInType === 'emailAddress',
|
||||
(identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
|
||||
).issuerAssignedId;
|
||||
|
||||
let status = USER_LICENSE_STATUS.NORMAL;
|
||||
|
||||
@ -7,6 +7,7 @@ import axios from 'axios';
|
||||
import { Aadb2cUser, B2cMetadata, JwkSignKey } from '../../common/token';
|
||||
import { AdB2cResponse, AdB2cUser } from './types/types';
|
||||
import { Context } from '../../common/log';
|
||||
import { ADB2C_SIGN_IN_TYPE } from '../../constants';
|
||||
|
||||
export type ConflictError = {
|
||||
reason: 'email';
|
||||
@ -74,7 +75,7 @@ export class AdB2cService {
|
||||
},
|
||||
identities: [
|
||||
{
|
||||
signinType: 'emailAddress',
|
||||
signinType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS,
|
||||
issuer: `${this.tenantName}.onmicrosoft.com`,
|
||||
issuerAssignedId: email,
|
||||
},
|
||||
|
||||
@ -27,6 +27,7 @@ import {
|
||||
import {
|
||||
LicenseSummaryInfo,
|
||||
PartnerLicenseInfoForRepository,
|
||||
PartnerInfoFromDb,
|
||||
} from '../../features/accounts/types/types';
|
||||
import { AccountNotFoundError } from './errors/types';
|
||||
import {
|
||||
@ -685,4 +686,83 @@ export class AccountsRepositoryService {
|
||||
await licenseRepo.delete({ order_id: targetOrder.id });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウントIDをもとに、パートナー一覧を取得する
|
||||
* @param id
|
||||
* @param limit
|
||||
* @param offset
|
||||
* @returns total: 総件数
|
||||
* @returns partners: DBから取得できるパートナー一覧情報
|
||||
*/
|
||||
async getPartners(
|
||||
id: number,
|
||||
limit: number,
|
||||
offset: number,
|
||||
): Promise<{
|
||||
total: number;
|
||||
partnersInfo: PartnerInfoFromDb[];
|
||||
}> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const accountRepo = entityManager.getRepository(Account);
|
||||
|
||||
// limit/offsetによらない総件数を取得する
|
||||
const total = await accountRepo.count({
|
||||
where: {
|
||||
parent_account_id: id,
|
||||
},
|
||||
});
|
||||
|
||||
const partnerAccounts = await accountRepo.find({
|
||||
where: {
|
||||
parent_account_id: id,
|
||||
},
|
||||
order: {
|
||||
company_name: 'ASC',
|
||||
},
|
||||
take: limit,
|
||||
skip: offset,
|
||||
});
|
||||
|
||||
// ADB2Cから情報を取得するための外部ユーザIDを取得する(念のためプライマリ管理者IDが存在しない場合を考慮)
|
||||
const primaryUserIds = partnerAccounts.map((x) => {
|
||||
if (x.primary_admin_user_id) {
|
||||
return x.primary_admin_user_id;
|
||||
} else if (x.secondary_admin_user_id) {
|
||||
return x.secondary_admin_user_id;
|
||||
}
|
||||
});
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const primaryUsers = await userRepo.find({
|
||||
where: {
|
||||
id: In(primaryUserIds),
|
||||
},
|
||||
});
|
||||
|
||||
// アカウント情報とプライマリ管理者の外部ユーザIDをマージ
|
||||
const partners = partnerAccounts.map((account) => {
|
||||
const primaryUser = primaryUsers.find(
|
||||
(user) =>
|
||||
user.id === account.primary_admin_user_id ||
|
||||
user.id === account.secondary_admin_user_id,
|
||||
);
|
||||
const primaryAccountExternalId = primaryUser
|
||||
? primaryUser.external_id
|
||||
: undefined;
|
||||
return {
|
||||
name: account.company_name,
|
||||
tier: account.tier,
|
||||
accountId: account.id,
|
||||
country: account.country,
|
||||
primaryAccountExternalId: primaryAccountExternalId,
|
||||
dealerManagement: account.delegation_permission,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
total: total,
|
||||
partnersInfo: partners,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user