Merged PR 310: API実装(ライセンス発行API)

## 概要
[Task2341: API実装(ライセンス発行API)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2341)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)
既存のaccounts.service.spec.tsのテスト

## レビューポイント
- 特にレビューしてほしい箇所
第一階層の場合とそれ以外の処理の違い

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認
①第一階層→第二階層から100件のライセンス注文を行い、ライセンス発行APIを呼び出し、
ライセンス注文テーブルの注文状態が発行済になることを確認。
ライセンステーブルが登録されることを確認。
DBの状態は、
第二階層のStock Licensesが100になることを確認。
登録・更新されたデータは処理単位で現在時刻が同じものであること
②第二階層→第三階層から90件のライセンス注文を行い、ライセンス発行APIを呼び出し、
ライセンス注文テーブルの注文状態が発行済になることを確認。
ライセンステーブルが登録されること(第三階層に対してUnAllocated)を確認。
ライセンステーブルが更新されること(第二階層に対してDeleted)を確認。
DBの状態は、
第二階層のStock Licensesが10になることを確認。
Deletedに更新されたライセンスについて、更新順がライセンスIDの順になっていることを確認。
第三階層のStock Licensesが90になることを確認。
登録・更新されたすべてのデータは、処理単位で現在時刻が同じものであることを確認。
③第五階層で呼び出した場合、エラーになることを確認。
④第4階層→第5階層に100件のライセンス注文を行い、ライセンス発行APIを呼び出し、
ライセンス数不足エラーとなることを確認。
⑤②で行ったライセンス発行APIを呼び出し、
ライセンス発行済みエラーとなることを確認。
⑥DBを起動していない状態で、ライセンス発行APIを呼び出し、
Internal Server Error 500が返却されることを確認。
## 補足
- 相談、参考資料などがあれば
This commit is contained in:
maruyama.t 2023-08-08 10:01:02 +00:00
parent ee783585ae
commit 01d20df628
9 changed files with 422 additions and 10 deletions

View File

@ -42,4 +42,6 @@ export const ErrorCodes = [
'E010701', // Blobファイル不在エラー
'E010801', // ライセンス不在エラー
'E010802', // ライセンス取り込み済みエラー
'E010803', // ライセンス発行済みエラー
'E010804', // ライセンス不足エラー
] as const;

View File

@ -31,4 +31,6 @@ export const errors: Errors = {
E010701: 'File not found in Blob Storage Error.',
E010801: 'License not exist Error',
E010802: 'License already activated Error',
E010803: 'License already issued Error',
E010804: 'License shortage Error',
};

View File

@ -40,7 +40,7 @@ import { RoleGuard } from '../../common/guards/role/roleguards';
import { retrieveAuthorizationToken } from '../../common/http/helper';
import { AccessToken } from '../../common/token';
import jwt from 'jsonwebtoken';
import { Context } from '../../common/log';
import { makeContext } from '../../common/log';
@ApiTags('accounts')
@Controller('accounts')
@ -401,12 +401,17 @@ export class AccountsController {
console.log(body);
const { orderedAccountId, poNumber } = body;
/*await this.licensesService.issueLicense(
orderedAccountId
const token = retrieveAuthorizationToken(req);
const accessToken = jwt.decode(token, { json: true }) as AccessToken;
const context = makeContext(accessToken.userId);
await this.accountService.issueLicense(
context,
orderedAccountId,
accessToken.userId,
accessToken.tier,
poNumber,
);
*/
return {};
}

View File

@ -14,12 +14,15 @@ import {
createAccount,
createLicense,
createLicenseOrder,
createUser,
createLicenseSetExpiryDateAndStatus,
} from './test/utility';
import { DataSource } from 'typeorm';
import { makeTestingModule } from '../../common/test/modules';
import { AccountsService } from './accounts.service';
import { makeContext } from '../../common/log';
import { TIERS } from '../../constants';
import { License } from '../../repositories/licenses/entity/license.entity';
describe('AccountsService', () => {
it('アカウントに紐づくライセンス情報を取得する', async () => {
@ -695,7 +698,7 @@ describe('getOrderHistories', () => {
});
});
describe('getDealers', () => {
describe('issueLicense', () => {
let source: DataSource = null;
beforeEach(async () => {
source = new DataSource({
@ -713,6 +716,189 @@ describe('getDealers', () => {
source = null;
});
it('指定した注文を発行済みにする', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const now = new Date();
// 親と子アカウントを作成する
const { accountId: parentAccountId } = await createAccount(
source,
0,
2,
'PARENTCORP',
);
const { accountId: childAccountId } = await createAccount(
source,
parentAccountId,
3,
'CHILDCORP1',
);
// 親と子のユーザーを作成する
const { externalId } = await createUser(
source,
parentAccountId,
'userId-parent',
'admin',
);
// 親のライセンスを作成する3個
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
// 子から親への注文を作成する2個
await createLicenseOrder(
source,
childAccountId,
parentAccountId,
2,
'TEST001',
new Date(now.getTime() + 60 * 60 * 1000),
);
const context = makeContext('userId-parent');
// 注文を発行済みにする
await service.issueLicense(
context,
childAccountId,
externalId,
2,
'TEST001',
);
const issuedLicenses = await source.getRepository(License).find({
where: {
account_id: childAccountId,
status: 'Unallocated',
type: 'NORMAL',
},
});
expect(issuedLicenses.length).toEqual(2);
});
it('既に注文が発行済みの場合、エラーとなる', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const now = new Date();
// 親と子アカウントを作成する
const { accountId: parentAccountId } = await createAccount(
source,
0,
2,
'PARENTCORP',
);
const { accountId: childAccountId } = await createAccount(
source,
parentAccountId,
3,
'CHILDCORP1',
);
// 親と子のユーザーを作成する
const { externalId } = await createUser(
source,
parentAccountId,
'userId-parent',
'admin',
);
// 親のライセンスを作成する3個
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
// 子から親への注文を作成する2個
await createLicenseOrder(
source,
childAccountId,
parentAccountId,
2,
'TEST001',
new Date(now.getTime() + 60 * 60 * 1000),
);
const context = makeContext('userId-parent');
// 注文を発行済みにする
await service.issueLicense(
context,
childAccountId,
externalId,
2,
'TEST001',
);
//再度同じ処理を行う
await expect(
service.issueLicense(context, childAccountId, externalId, 2, 'TEST001'),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010803'), HttpStatus.BAD_REQUEST),
);
});
it('ライセンスが不足している場合、エラーとなる', async () => {
const module = await makeTestingModule(source);
const service = module.get<AccountsService>(AccountsService);
const now = new Date();
// 親と子アカウントを作成する
const { accountId: parentAccountId } = await createAccount(
source,
0,
2,
'PARENTCORP',
);
const { accountId: childAccountId } = await createAccount(
source,
parentAccountId,
3,
'CHILDCORP1',
);
// 親と子のユーザーを作成する
const { externalId } = await createUser(
source,
parentAccountId,
'userId-parent',
'admin',
);
// 親のライセンスを作成する3個
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
// 子から親への注文を作成する4個
await createLicenseOrder(
source,
childAccountId,
parentAccountId,
4,
'TEST001',
new Date(now.getTime() + 60 * 60 * 1000),
);
const context = makeContext('userId-parent');
// 注文を発行済みにする
await expect(
service.issueLicense(context, childAccountId, externalId, 2, 'TEST001'),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010804'), HttpStatus.BAD_REQUEST),
);
});
});
describe('getDealers', () => {
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('Dealerを取得できる', async () => {
const module = await makeTestingModule(source);
const { accountId: accountId_1 } = await createAccount(

View File

@ -37,6 +37,12 @@ import { UserGroupsRepositoryService } from '../../repositories/user_groups/user
import { makePassword } from '../../common/password';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
import { Context } from '../../common/log';
import {
LicensesShortageError,
AlreadyIssuedError,
OrderNotFoundError,
} from '../../repositories/licenses/errors/types';
@Injectable()
export class AccountsService {
constructor(
@ -579,6 +585,72 @@ export class AccountsService {
}
}
/**
*
* @param context
* @param orderedAccountId
* @param userId
* @param tier
* @param poNumber
*/
async issueLicense(
context: Context,
orderedAccountId: number,
userId: string,
tier: number,
poNumber: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.issueLicense.name} | params: { ` +
`orderedAccountId: ${orderedAccountId}, ` +
`userId: ${userId}, ` +
`tier: ${tier}, ` +
`poNumber: ${poNumber} };`,
);
try {
// アクセストークンからユーザーIDを取得する
const myAccountId = (
await this.usersRepository.findUserByExternalId(userId)
).account_id;
await this.licensesRepository.issueLicense(
orderedAccountId,
myAccountId,
tier,
poNumber,
);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case OrderNotFoundError:
throw new HttpException(
makeErrorResponse('E010801'),
HttpStatus.BAD_REQUEST,
);
case AlreadyIssuedError:
throw new HttpException(
makeErrorResponse('E010803'),
HttpStatus.BAD_REQUEST,
);
case LicensesShortageError:
throw new HttpException(
makeErrorResponse('E010804'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
} finally {
this.logger.log(
`[OUT] [${context.trackingId}] ${this.issueLicense.name}`,
);
}
}
// dealersのアカウント情報を取得する
async getDealers(): Promise<GetDealersResponse> {
this.logger.log(`[IN] ${this.getDealers.name}`);

View File

@ -14,6 +14,7 @@ import { UserGroup } from '../../../repositories/user_groups/entity/user_group.e
import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service';
import { AdB2cUser } from '../../../gateways/adb2c/types/types';
import { LicensesRepositoryService } from '../../../repositories/licenses/licenses.repository.service';
import { Context } from '../../../common/log';
export type LicensesRepositoryMockValue = {
getLicenseOrderHistoryInfo:
@ -22,6 +23,7 @@ export type LicensesRepositoryMockValue = {
orderHistories: LicenseOrder[];
}
| Error;
issueLicense: undefined | Error;
};
export type UsersRepositoryMockValue = {
findUserById: User | Error;
@ -127,10 +129,10 @@ export const makeAccountsRepositoryMock = (
export const makeLicensesRepositoryMock = (
value: LicensesRepositoryMockValue,
) => {
const { getLicenseOrderHistoryInfo } = value;
const { getLicenseOrderHistoryInfo, issueLicense } = value;
return {
findUserById:
getLicenseOrderHistoryInfo:
getLicenseOrderHistoryInfo instanceof Error
? jest
.fn<Promise<void>, []>()
@ -141,6 +143,21 @@ export const makeLicensesRepositoryMock = (
[]
>()
.mockResolvedValue(getLicenseOrderHistoryInfo),
issueLicense:
issueLicense instanceof Error
? jest.fn<Promise<void>, []>().mockRejectedValue(issueLicense)
: jest
.fn<
Promise<{
context: Context;
orderedAccountId: number;
myAccountId: number;
tier: number;
poNumber: string;
}>,
[]
>()
.mockResolvedValue(issueLicense),
};
};
export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => {
@ -402,5 +419,6 @@ export const makeDefaultLicensesRepositoryMockValue =
},
],
},
issueLicense: undefined,
};
};

View File

@ -4,6 +4,7 @@ import {
License,
LicenseOrder,
} from '../../../repositories/licenses/entity/license.entity';
import { User } from '../../../repositories/users/entity/user.entity';
export const createAccount = async (
datasource: DataSource,
@ -98,3 +99,31 @@ export const createLicenseOrder = async (
});
identifiers.pop() as License;
};
export const createUser = async (
datasource: DataSource,
accountId: number,
external_id: string,
role: string,
author_id?: string | undefined,
): Promise<{ userId: number; externalId: string; authorId: string }> => {
const { identifiers } = await datasource.getRepository(User).insert({
account_id: accountId,
external_id: external_id,
role: role,
accepted_terms_version: '1.0',
author_id: author_id,
email_verified: true,
auto_renew: true,
license_alert: true,
notification: true,
encryption: false,
prompt: false,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
updated_at: new Date(),
});
const user = identifiers.pop() as User;
return { userId: user.id, externalId: external_id, authorId: author_id };
};

View File

@ -6,3 +6,10 @@ export class LicenseNotExistError extends Error {}
// 取り込むライセンスが既に取り込み済みのエラー
export class LicenseKeyAlreadyActivatedError extends Error {}
// 注文不在エラー
export class OrderNotFoundError extends Error {}
// 注文発行済エラー
export class AlreadyIssuedError extends Error {}
// ライセンス不足エラー
export class LicensesShortageError extends Error {}

View File

@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { DataSource, FindManyOptions, In } from 'typeorm';
import {
LicenseOrder,
License,
@ -12,11 +12,15 @@ import {
LICENSE_STATUS_ISSUE_REQUESTING,
LICENSE_STATUS_ISSUED,
LICENSE_TYPE,
TIERS,
} from '../../constants';
import {
PoNumberAlreadyExistError,
LicenseNotExistError,
LicenseKeyAlreadyActivatedError,
LicensesShortageError,
AlreadyIssuedError,
OrderNotFoundError,
} from './errors/types';
@Injectable()
@ -296,4 +300,91 @@ export class LicensesRepositoryService {
};
});
}
/**
*
* @context Context
* @param orderedAccountId
* @param myAccountId
* @param tier
* @param poNumber
*/
async issueLicense(
orderedAccountId: number,
myAccountId: number,
tier: number,
poNumber: string,
): Promise<void> {
const nowDate = new Date();
await this.dataSource.transaction(async (entityManager) => {
const licenseOrderRepo = entityManager.getRepository(LicenseOrder);
const licenseRepo = entityManager.getRepository(License);
const issuingOrder = await licenseOrderRepo.findOne({
where: {
from_account_id: orderedAccountId,
po_number: poNumber,
},
});
// 注文が存在しない場合、エラー
if (!issuingOrder) {
throw new OrderNotFoundError(`No order found for PONumber:${poNumber}`);
}
// 既に発行済みの注文の場合、エラー
if (issuingOrder.status !== LICENSE_STATUS_ISSUE_REQUESTING) {
throw new AlreadyIssuedError(
`An order for PONumber:${poNumber} has already been issued.`,
);
}
// ライセンステーブルのレコードを作成する
const newLicenses = Array.from({ length: issuingOrder.quantity }, () => {
const license = new License();
license.account_id = orderedAccountId;
license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
license.type = LICENSE_TYPE.NORMAL;
license.order_id = issuingOrder.id;
return license;
});
// ライセンス注文テーブルを更新(注文元)
await licenseOrderRepo.update(
{ id: issuingOrder.id },
{
issued_at: nowDate,
status: LICENSE_STATUS_ISSUED,
},
);
// ライセンステーブルを登録(注文元)
await licenseRepo.save(newLicenses);
// 第一階層の場合はストックライセンスの概念が存在しないため、ストックライセンス変更処理は行わない
if (tier !== TIERS.TIER1) {
const licensesToUpdate = await licenseRepo.find({
where: {
account_id: myAccountId,
status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
type: LICENSE_TYPE.NORMAL,
},
order: {
id: 'ASC',
},
take: newLicenses.length,
});
// 登録したライセンスに対して自身のライセンスが不足していた場合、エラー
if (newLicenses.length > licensesToUpdate.length) {
throw new LicensesShortageError(
`Shortage Licenses.Number of licenses attempted to be issued is ${newLicenses.length}.`,
);
}
for (const licenseToUpdate of licensesToUpdate) {
licenseToUpdate.status = LICENSE_ALLOCATED_STATUS.DELETED;
licenseToUpdate.deleted_at = nowDate;
licenseToUpdate.delete_order_id = issuingOrder.id;
}
// 自身のライセンスを削除(論理削除)する
await licenseRepo.save(licensesToUpdate);
}
});
}
}