Merged PR 254: API実装(パートナーライセンス情報取得API)

## 概要
[Task2213: API実装(パートナーライセンス情報取得API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2213)

- パートナーライセンス情報取得APIについて実装を行いました
- このPull Request機能としては対象外。気づいたので直した部分
 ・accounts.service.mock.ts
  →整形されていなかったのを修正、不要な参照を削除
 ・accounts.service.spec.ts
  →整形されていなかったのを修正、不要な参照(userInfo)を削除
 ・license.entity.ts
  →テスト実施のため必須項目の定義を追加
 ・dev-database-rg.json
  →developからのマージだが、差分ファイルとして出てしまっている。
   (誤ってコミットのstagingを解除してしまい、後からpull操作したことが影響していると思われる)
- 影響範囲(他の機能にも影響があるか)
 →特になし

## レビューポイント
- MISOチームメンバについては、「このPull Request機能としては対象外。気づいたので直した部分」の確認をお願いします。整形や不要な参照はMISOで作りこんだものです。
- accounts.repository.service.tsについて、DRYの観点で一部処理をprivateなサブルーチンとしています(getAccountLicenseOrderStatus)。そもそもそういうことをやってよいか、分ける際のやりかた(引数など)について妥当か見ていただきたいです。
- repositoryからの戻り値型の名称についてわかりやすいかどうか。「PartnerLicenseInfoのうちリポジトリから取得した情報」として「PartnerLicenseInfoFromRepository」としていますが、Fromとかつけない方がよいのでは、とか呼び元を意識しすぎた名称になってる、とか気にしています。

## UIの変更
- 無し

## 動作確認状況
- ローカルで確認済

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
masaaki 2023-07-25 02:08:20 +00:00
parent 63109d9857
commit 70cb66e517
10 changed files with 6393 additions and 5997 deletions

File diff suppressed because it is too large Load Diff

View File

@ -319,42 +319,10 @@ export class AccountsController {
@Body() body: GetPartnerLicensesRequest,
): Promise<GetPartnerLicensesResponse> {
const { limit, offset, accountId } = body;
const accessToken = retrieveAuthorizationToken(req);
const payload = jwt.decode(accessToken, { json: true }) as AccessToken;
// API未実装のうちはサンプルをreturn
const ret: GetPartnerLicensesResponse = {
total: 2,
ownPartnerLicense: {
accountId: 1,
tier: 2,
companyName: 'testAccount',
stockLicense: 30,
issuedRequested: 5,
shortage: 25,
issueRequesting: 100,
},
childrenPartnerLicenses: [
{
accountId: 10,
tier: 3,
companyName: 'testChild',
stockLicense: 300,
issuedRequested: 50,
shortage: 250,
issueRequesting: 1000,
},
{
accountId: 20,
tier: 3,
companyName: 'testChild2',
stockLicense: 700,
issuedRequested: 500,
shortage: 200,
issueRequesting: 51000,
},
],
};
return ret;
const getPartnerLicensesResponse =
await this.accountService.getPartnerLicenses(limit, offset, accountId);
return getPartnerLicensesResponse;
}
}

View File

@ -9,6 +9,14 @@ import {
makeDefaultUsersRepositoryMockValue,
} from './test/accounts.service.mock';
import { makeDefaultConfigValue } from '../users/test/users.service.mock';
import {
createAccount,
createLicense,
createLicenseOrder,
} from './test/utility';
import { DataSource } from 'typeorm';
import { makeTestingModule } from '../../common/test/modules';
import { AccountsService } from './accounts.service';
describe('AccountsService', () => {
it('アカウントに紐づくライセンス情報を取得する', async () => {
@ -308,20 +316,84 @@ const expectedAccountLisenceCounts = {
isStorageAvailable: false,
};
const userInfo = {
accepted_terms_version: '1.0',
account_id: 1234567890123456,
author_id: '6cce347f-0cf1-a15e-19ab-d00988b643f9',
auto_renew: false,
created_at: new Date('2023-06-13 00:00:00'),
created_by: 'test',
deleted_at: null,
email_verified: true,
external_id: 'ede66c43-9b9d-4222-93ed-5f11c96e08e2',
id: 1,
license_alert: false,
notification: false,
role: 'none admin',
updated_at: null,
updated_by: null,
};
describe('createPartnerAccount', () => {
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);
// 親アカウントと子アカウント2つ作成
const { accountId: parentAccountId } = await createAccount(
source,
0,
1,
'PARENTCORP',
);
const { accountId: childAccountId1 } = await createAccount(
source,
parentAccountId,
2,
'CHILDCORP1',
);
const { accountId: childAccountId2 } = await createAccount(
source,
parentAccountId,
2,
'CHILDCORP2',
);
// 所有ライセンスを追加3、子11、子22
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(source, childAccountId1);
await createLicense(source, childAccountId2);
await createLicense(source, childAccountId2);
// ライセンス注文を追加子1→親10ライセンス、子2→親5ライセンス
await createLicenseOrder(source, childAccountId1, parentAccountId, 10);
await createLicenseOrder(source, childAccountId2, parentAccountId, 5);
const service = module.get<AccountsService>(AccountsService);
const accountId = parentAccountId;
const offset = 0;
const limit = 20;
const responce = await service.getPartnerLicenses(limit, offset, accountId);
expect(responce.total).toBe(2);
expect(responce.ownPartnerLicense.companyName).toBe('PARENTCORP');
expect(responce.ownPartnerLicense.tier).toBe(1);
expect(responce.ownPartnerLicense.stockLicense).toBe(3);
expect(responce.ownPartnerLicense.issuedRequested).toBe(15);
expect(responce.ownPartnerLicense.shortage).toBe(12);
expect(responce.childrenPartnerLicenses[0].companyName).toBe('CHILDCORP1');
expect(responce.childrenPartnerLicenses[0].tier).toBe(2);
expect(responce.childrenPartnerLicenses[0].stockLicense).toBe(1);
expect(responce.childrenPartnerLicenses[0].issueRequesting).toBe(10);
expect(responce.childrenPartnerLicenses[1].companyName).toBe('CHILDCORP2');
expect(responce.childrenPartnerLicenses[1].tier).toBe(2);
expect(responce.childrenPartnerLicenses[1].stockLicense).toBe(2);
expect(responce.childrenPartnerLicenses[1].issueRequesting).toBe(5);
});
});

View File

@ -16,7 +16,12 @@ import {
USER_ROLES,
} from '../../constants';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { TypistGroup } from './types/types';
import {
TypistGroup,
GetPartnerLicensesResponse,
PartnerLicenseInfo,
} from './types/types';
import { DateWithZeroTime } from '../licenses/types/types';
import { GetLicenseSummaryResponse, Typist } from './types/types';
import { AccessToken } from '../../common/token';
import { UserNotFoundError } from '../../repositories/users/errors/types';
@ -45,9 +50,7 @@ export class AccountsService {
this.logger.log(`[IN] ${this.getLicenseSummary.name}`);
try {
const currentDate = new Date();
// 有効期限との比較は時間まで見ず日付だけで判別するため、各値0をセット
currentDate.setHours(0, 0, 0, 0);
const currentDate = new DateWithZeroTime();
const expiringSoonDate = new Date(currentDate.getTime());
expiringSoonDate.setDate(
@ -390,4 +393,83 @@ export class AccountsService {
);
}
}
/**
*
* @param limit
* @param offset
* @param accountId
* @returns getPartnerLicensesResponse
*/
async getPartnerLicenses(
limit: number,
offset: number,
accountId: number,
): Promise<GetPartnerLicensesResponse> {
this.logger.log(`[IN] ${this.getPartnerLicenses.name}`);
try {
const currentDate = new DateWithZeroTime();
const getPartnerLicenseResult =
await this.accountRepository.getPartnerLicense(
accountId,
currentDate,
offset,
limit,
);
// 自アカウントのShortageを算出してreturn用の変数にマージする
let ownShortage =
getPartnerLicenseResult.ownPartnerLicenseFromRepository.stockLicense -
getPartnerLicenseResult.ownPartnerLicenseFromRepository.issuedRequested;
// 「不足している値」を取得するため、負数の場合は絶対値とし、0以上の場合は0とする
ownShortage = ownShortage >= 0 ? 0 : Math.abs(ownShortage);
// return用の型にリポジトリから取得した型をマージし、不足項目shortageを設定する
const ownPartnerLicense: PartnerLicenseInfo = Object.assign(
{},
getPartnerLicenseResult.ownPartnerLicenseFromRepository,
{
shortage: ownShortage,
},
);
// 各子アカウントのShortageを算出してreturn用の変数にマージする
const childrenPartnerLicenses: PartnerLicenseInfo[] = [];
for (const childPartnerLicenseFromRepository of getPartnerLicenseResult.childPartnerLicensesFromRepository) {
let childShortage =
childPartnerLicenseFromRepository.stockLicense -
childPartnerLicenseFromRepository.issuedRequested;
// 「不足している値」を取得するため、負数の場合は絶対値とし、0以上の場合は0とする
childShortage = childShortage >= 0 ? 0 : Math.abs(childShortage);
const childPartnerLicense: PartnerLicenseInfo = Object.assign(
{},
childPartnerLicenseFromRepository,
{
shortage: childShortage,
},
);
childrenPartnerLicenses.push(childPartnerLicense);
}
const getPartnerLicensesResponse: GetPartnerLicensesResponse = {
total: getPartnerLicenseResult.total,
ownPartnerLicense: ownPartnerLicense,
childrenPartnerLicenses: childrenPartnerLicenses,
};
return getPartnerLicensesResponse;
} catch (e) {
this.logger.error(e);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(`[OUT] ${this.getPartnerLicenses.name}`);
}
}
}

View File

@ -13,7 +13,6 @@ import { Account, LicenseSummaryInfo } from '../types/types';
import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity';
import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service';
import { AdB2cUser } from '../../../gateways/adb2c/types/types';
import { AccountSASPermissions } from '@azure/storage-blob';
export type UsersRepositoryMockValue = {
findUserById: User | Error;
findUserByExternalId: User | Error;

View File

@ -0,0 +1,74 @@
import { DataSource } from 'typeorm';
import { Account } from '../../../repositories/accounts/entity/account.entity';
import {
License,
LicenseOrder,
} from '../../../repositories/licenses/entity/license.entity';
export const createAccount = async (
datasource: DataSource,
parentAccountId: number,
tier: number,
companyName: string,
): Promise<{ accountId: number }> => {
const { identifiers } = await datasource.getRepository(Account).insert({
parent_account_id: parentAccountId,
tier: tier,
country: 'JP',
delegation_permission: false,
locked: false,
company_name: companyName,
verified: true,
deleted_at: '',
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
const account = identifiers.pop() as Account;
return { accountId: account.id };
};
export const createLicense = async (
datasource: DataSource,
accountId: number,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({
expiry_date: null,
account_id: accountId,
type: 'NORMAL',
status: 'Unallocated',
allocated_user_id: null,
order_id: null,
deleted_at: null,
delete_order_id: null,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
identifiers.pop() as License;
};
export const createLicenseOrder = async (
datasource: DataSource,
fromAccountId: number,
toAccountId: number,
quantity: number,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(LicenseOrder).insert({
po_number: 'TEST123',
from_account_id: fromAccountId,
to_account_id: toAccountId,
ordered_at: new Date(),
issued_at: null,
quantity: quantity,
status: 'Issue Requesting',
canceled_at: null,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
identifiers.pop() as License;
};

View File

@ -186,3 +186,9 @@ export class GetPartnerLicensesResponse {
@ApiProperty({ type: [PartnerLicenseInfo] })
childrenPartnerLicenses: PartnerLicenseInfo[];
}
// RepositoryからPartnerLicenseInfoに関する情報を取得する際の型
export type PartnerLicenseInfoForRepository = Omit<
PartnerLicenseInfo,
'shortage'
>;

View File

@ -36,3 +36,15 @@ export class ActivateCardLicensesRequest {
}
export class ActivateCardLicensesResponse {}
// ライセンス算出用に、その日の始まりの時刻0:00:00.000)の日付を取得する
export class DateWithZeroTime extends Date {
constructor(...args: any[]) {
if (args.length === 0) {
super(); // 引数がない場合、現在の日付で初期化
} else {
super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す
}
this.setHours(0, 0, 0, 0); // 時分秒を"0:00:00.000"に固定
}
}

View File

@ -8,6 +8,7 @@ import {
MoreThanOrEqual,
Not,
UpdateResult,
EntityManager,
} from 'typeorm';
import { User } from '../users/entity/user.entity';
import { Account } from './entity/account.entity';
@ -21,7 +22,10 @@ import {
LICENSE_ALLOCATED_STATUS,
LICENSE_STATUS_ISSUE_REQUESTING,
} from '../../constants';
import { LicenseSummaryInfo } from '../../features/accounts/types/types';
import {
LicenseSummaryInfo,
PartnerLicenseInfoForRepository,
} from '../../features/accounts/types/types';
import { AccountNotFoundError } from './errors/types';
@Injectable()
@ -290,4 +294,171 @@ export class AccountsRepositoryService {
return { licenseSummary: licenseSummary, isStorageAvailable };
});
}
/**
*
* IDをもとに
* @param id
* @param currentDate
* @param entityManager
* @returns stockLicense
* @returns issuedRequested
* @returns issueRequesting
*/
private async getAccountLicenseOrderStatus(
id: number,
currentDate: Date,
entityManager: EntityManager,
): Promise<{
stockLicense: number;
issuedRequested: number;
issueRequesting: number;
}> {
const license = entityManager.getRepository(License);
const licenseOrder = entityManager.getRepository(LicenseOrder);
// 有効な総ライセンス数を取得する
const stockLicense = await license.count({
where: [
{
account_id: id,
expiry_date: MoreThanOrEqual(currentDate),
status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
},
{
account_id: id,
expiry_date: IsNull(),
status: Not(LICENSE_ALLOCATED_STATUS.DELETED),
},
],
});
// 子アカウントからの、未発行状態あるいは発行キャンセルされた注文の総ライセンス数を取得する
const issuedRequestedSqlResult = await licenseOrder
.createQueryBuilder('license_orders')
.select('SUM(license_orders.quantity)', 'sum')
.where('license_orders.to_account_id = :id', { id })
.andWhere('license_orders.status = :status', {
status: LICENSE_STATUS_ISSUE_REQUESTING,
})
.getRawOne();
const issuedRequested = parseInt(issuedRequestedSqlResult.sum, 10) || 0;
// 未発行状態あるいは発行キャンセルされた注文の総ライセンス数を取得する
const issuedRequestingSqlResult = await licenseOrder
.createQueryBuilder('license_orders')
.select('SUM(license_orders.quantity)', 'sum')
.where('license_orders.from_account_id = :id', { id })
.andWhere('license_orders.status = :status', {
status: LICENSE_STATUS_ISSUE_REQUESTING,
})
.getRawOne();
const issuedRequesting = parseInt(issuedRequestingSqlResult.sum, 10) || 0;
return {
stockLicense: stockLicense,
issuedRequested: issuedRequested,
issueRequesting: issuedRequesting,
};
}
/**
* IDをもとに
* @param id
* @param offset
* @param limit
* @returns total: 総件数
* @returns ownPartnerLicenseFromRepository: リポジトリから取得した自アカウントのライセンス情報
* @returns childrenPartnerLicensesFromRepository: リポジトリから取得した子アカウントのライセンス情報
*/
async getPartnerLicense(
id: number,
currentDate: Date,
offset: number,
limit: number,
): Promise<{
total: number;
ownPartnerLicenseFromRepository: PartnerLicenseInfoForRepository;
childPartnerLicensesFromRepository: PartnerLicenseInfoForRepository[];
}> {
return await this.dataSource.transaction(async (entityManager) => {
const account = entityManager.getRepository(Account);
// 自アカウントの情報を取得する
const ownAccount = await account.findOne({
where: {
id: id,
},
});
// 自アカウントのライセンス注文状況を取得する
const ownLicenseOrderStatus = await this.getAccountLicenseOrderStatus(
id,
currentDate,
entityManager,
);
// 自アカウントの戻り値を設定する
const ownPartnerLicenseFromRepository: PartnerLicenseInfoForRepository = {
accountId: id,
tier: ownAccount.tier,
companyName: ownAccount.company_name,
stockLicense: ownLicenseOrderStatus.stockLicense,
issuedRequested: ownLicenseOrderStatus.issuedRequested,
issueRequesting: ownLicenseOrderStatus.issueRequesting,
};
// 子アカウントのアカウント情報を取得する
const childAccounts = await account.find({
where: {
parent_account_id: id,
},
order: {
company_name: 'ASC',
},
take: limit,
skip: offset,
});
// 各子アカウントのライセンス注文状況を取得する
const childPartnerLicensesFromRepository: PartnerLicenseInfoForRepository[] =
[];
for (const childAccount of childAccounts) {
// ライセンス注文状況を取得する
const childLicenseOrderStatus = await this.getAccountLicenseOrderStatus(
childAccount.id,
currentDate,
entityManager,
);
// 戻り値用の値を設定
const childPartnerLicenseFromRepository: PartnerLicenseInfoForRepository =
{
accountId: childAccount.id,
tier: childAccount.tier,
companyName: childAccount.company_name,
stockLicense: childLicenseOrderStatus.stockLicense,
issuedRequested: childLicenseOrderStatus.issuedRequested,
issueRequesting: childLicenseOrderStatus.issueRequesting,
};
childPartnerLicensesFromRepository.push(
childPartnerLicenseFromRepository,
);
}
// limit/offsetによらない総件数を取得する
const total = await account.count({
where: {
parent_account_id: id,
},
});
return {
total: total,
ownPartnerLicenseFromRepository: ownPartnerLicenseFromRepository,
childPartnerLicensesFromRepository: childPartnerLicensesFromRepository,
};
});
}
}

View File

@ -34,6 +34,18 @@ export class LicenseOrder {
@Column({ nullable: true })
canceled_at?: Date;
@Column({ nullable: true })
created_by: string;
@CreateDateColumn()
created_at: Date;
@Column({ nullable: true })
updated_by: string;
@UpdateDateColumn()
updated_at: Date;
}
@Entity({ name: 'licenses' })