Merged PR 378: API実装(ライセンス発行キャンセルAPI)

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

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
ライセンス発行をキャンセルするAPIを実装
下位のアカウント情報と、上位のアカウント情報をセットすると、パートナー関係であるかを返す関数を追加
既存のユニットテストのライセンス作成箇所で、注文ID、削除日時、削除注文IDを指定できるように修正
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)
既存ユニットテストのライセンス作成部分

## レビューポイント
- 特にレビューしてほしい箇所
パートナー関係かどうかを返す箇所、共通的に使いやすいかどうか
14日より経過していた場合の箇所、ライセンスの有効期限の定数を使っているが分けたほうが良いか
## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認
■正常系
ライセンス発行のキャンセルが完了できる(第一階層で実行)
ライセンス発行のキャンセルが完了できる(第二階層で実行)
キャンセルした発行の注文状態が発行待ちに戻る
発行されたライセンスは物理削除される
論理削除されていたライセンスは未割当で、削除前の状態に戻る
■異常系
第一、第二階層以外で実行した場合はエラー
キャンセル対象の発行が存在しない場合エラー
キャンセル対象の発行が14日より経過していた場合はエラー
キャンセル対象の発行のライセンスが使われていた場合はエラー
自身のパートナー以外の発行をキャンセルしようとした場合、エラー

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
maruyama.t 2023-09-05 05:17:47 +00:00
parent 2d378b8e66
commit 7524abbae6
11 changed files with 817 additions and 36 deletions

View File

@ -48,6 +48,9 @@ export const ErrorCodes = [
'E010806', // ライセンス割り当て不可エラー
'E010807', // ライセンス割り当て解除済みエラー
'E010808', // ライセンス注文キャンセル不可エラー
'E010809', // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
'E010810', // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
'E010908', // タイピストグループ不在エラー
'E011001', // ワークタイプ重複エラー
'E011002', // ワークタイプ登録上限超過エラー

View File

@ -37,6 +37,9 @@ export const errors: Errors = {
E010806: 'License is unavailable Error',
E010807: 'License is already deallocated Error',
E010808: 'Order cancel failed Error',
E010809: 'Already license order status changed Error',
E010810: 'Cancellation period expired error',
E010811: 'Already license allocated Error',
E010908: 'Typist Group not exist Error',
E011001: 'Thiw WorkTypeID already used Error',
E011002: 'WorkTypeID create limit exceeded Error',

View File

@ -636,12 +636,12 @@ export class AccountsController {
const context = makeContext(payload.userId);
// TODO: 発行キャンセル処理。API実装のタスク2498で本実装
// await this.accountService.cancelIssue(
// context,
// body.poNumber,
// body.orderedAccountId,
// );
await this.accountService.cancelIssue(
context,
payload.userId,
body.poNumber,
body.orderedAccountId,
);
return {};
}

View File

@ -32,10 +32,14 @@ import {
getUserFromExternalId,
getUsers,
makeTestUser,
makeHierarchicalAccounts,
} from '../../common/test/utility';
import { AccountsService } from './accounts.service';
import { Context, makeContext } from '../../common/log';
import {
LICENSE_ALLOCATED_STATUS,
LICENSE_ISSUE_STATUS,
LICENSE_TYPE,
OPTION_ITEM_VALUE_TYPE,
TIERS,
USER_ROLES,
@ -51,8 +55,12 @@ import {
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
import {
createOrder,
selectLicense,
selectOrderLicense,
} from '../licenses/test/utility';
import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service';
import exp from 'constants';
describe('createAccount', () => {
let source: DataSource = null;
@ -1827,14 +1835,79 @@ describe('getPartnerAccount', () => {
).account;
// 所有ライセンスを追加3、子11、子22
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(
source,
1,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
2,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
3,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
4,
null,
childAccountId1,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
2,
null,
null,
);
await createLicense(source, childAccountId1);
await createLicense(source, childAccountId2);
await createLicense(source, childAccountId2);
await createLicense(
source,
5,
null,
childAccountId2,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
3,
null,
null,
);
await createLicense(
source,
6,
null,
childAccountId2,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
3,
null,
null,
);
// ライセンス注文を追加子1→親10ライセンス、子2→親5ライセンス
await createLicenseOrder(
@ -1980,7 +2053,12 @@ describe('getPartnerAccount', () => {
}
// 有効期限未設定のライセンスを1件追加子1
await createLicense(source, childAccountId1);
await createLicenseSetExpiryDateAndStatus(
source,
childAccountId1,
null,
'Unallocated',
);
const service = module.get<AccountsService>(AccountsService);
const accountId = parentAccountId;
@ -2172,9 +2250,42 @@ describe('issueLicense', () => {
});
// 親のライセンスを作成する3個
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(
source,
1,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
2,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
3,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
// 子から親への注文を作成する2個
await createLicenseOrder(
source,
@ -2231,9 +2342,42 @@ describe('issueLicense', () => {
role: 'admin',
});
// 親のライセンスを作成する3個
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(
source,
1,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
2,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
3,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
// 子から親への注文を作成する2個
await createLicenseOrder(
source,
@ -2289,9 +2433,42 @@ describe('issueLicense', () => {
});
// 親のライセンスを作成する3個
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(source, parentAccountId);
await createLicense(
source,
1,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
2,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
await createLicense(
source,
3,
null,
parentAccountId,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
// 子から親への注文を作成する4個
await createLicenseOrder(
source,
@ -3414,3 +3591,297 @@ describe('createWorktype', () => {
}
});
});
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 { tier1Accounts: tier1Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const poNumber = 'CANCEL_TEST';
const date = new Date();
date.setDate(date.getDate() - 10);
await createOrder(
source,
poNumber,
tier5Accounts.account.id,
tier5Accounts.account.parent_account_id,
date,
1,
LICENSE_ISSUE_STATUS.ISSUED,
);
date.setDate(date.getDate() + 10);
// 発行時に論理削除されたライセンス情報
await createLicense(
source,
1,
date,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
date,
1,
);
const service = module.get<AccountsService>(AccountsService);
await service.cancelIssue(
makeContext('trackingId'),
tier1Accounts[0].users[0].external_id,
poNumber,
tier5Accounts.account.id,
);
// 発行待ちに戻した注文の状態確認
const orderRecord = await selectOrderLicense(
source,
tier5Accounts.account.id,
poNumber,
);
expect(orderRecord.orderLicense.issued_at).toBe(null);
expect(orderRecord.orderLicense.status).toBe(
LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
);
// 未割当に戻したライセンスの状態確認
const licenseRecord = await selectLicense(source, 1);
expect(licenseRecord.license.status).toBe(
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
);
expect(licenseRecord.license.delete_order_id).toBe(null);
expect(licenseRecord.license.deleted_at).toBe(null);
});
it('ライセンス発行のキャンセルが完了する(第二階層で実行)', async () => {
const module = await makeTestingModule(source);
const { tier2Accounts: tier2Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const poNumber = 'CANCEL_TEST';
const date = new Date();
date.setDate(date.getDate() - 10);
await createOrder(
source,
poNumber,
tier5Accounts.account.id,
tier5Accounts.account.parent_account_id,
date,
1,
LICENSE_ISSUE_STATUS.ISSUED,
);
date.setDate(date.getDate() + 10);
// 発行時に論理削除されたライセンス情報
await createLicense(
source,
1,
date,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
date,
1,
);
const service = module.get<AccountsService>(AccountsService);
await service.cancelIssue(
makeContext('trackingId'),
tier2Accounts[0].users[0].external_id,
poNumber,
tier5Accounts.account.id,
);
// 発行待ちに戻した注文の状態確認
const orderRecord = await selectOrderLicense(
source,
tier5Accounts.account.id,
poNumber,
);
expect(orderRecord.orderLicense.issued_at).toBe(null);
expect(orderRecord.orderLicense.status).toBe(
LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
);
// 未割当に戻したライセンスの状態確認
const licenseRecord = await selectLicense(source, 1);
expect(licenseRecord.license.status).toBe(
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
);
expect(licenseRecord.license.delete_order_id).toBe(null);
expect(licenseRecord.license.deleted_at).toBe(null);
});
it('キャンセル対象の発行が存在しない場合エラー', async () => {
const module = await makeTestingModule(source);
const { tier1Accounts: tier1Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const poNumber = 'CANCEL_TEST';
const service = module.get<AccountsService>(AccountsService);
await expect(
service.cancelIssue(
makeContext('trackingId'),
tier1Accounts[0].users[0].external_id,
poNumber,
tier5Accounts.account.id,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010809'), HttpStatus.BAD_REQUEST),
);
});
it('キャンセル対象の発行が14日より経過していた場合エラー', async () => {
const module = await makeTestingModule(source);
const { tier1Accounts: tier1Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const poNumber = 'CANCEL_TEST';
const date = new Date();
date.setDate(date.getDate() - 15);
await createOrder(
source,
poNumber,
tier5Accounts.account.id,
tier5Accounts.account.parent_account_id,
date,
1,
LICENSE_ISSUE_STATUS.ISSUED,
);
await createLicense(
source,
1,
date,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
const service = module.get<AccountsService>(AccountsService);
await expect(
service.cancelIssue(
makeContext('trackingId'),
tier1Accounts[0].users[0].external_id,
poNumber,
tier5Accounts.account.id,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010810'), HttpStatus.BAD_REQUEST),
);
});
it('キャンセル対象の発行のライセンスが使われていた場合エラー', async () => {
const module = await makeTestingModule(source);
const { tier1Accounts: tier1Accounts, tier4Accounts: tier4Accounts } =
await makeHierarchicalAccounts(source);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: tier4Accounts[0].account.id,
tier: 5,
});
const poNumber = 'CANCEL_TEST';
const date = new Date();
date.setDate(date.getDate() - 14);
await createOrder(
source,
poNumber,
tier5Accounts.account.id,
tier5Accounts.account.parent_account_id,
date,
1,
LICENSE_ISSUE_STATUS.ISSUED,
);
await createLicense(
source,
1,
date,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
null,
1,
null,
null,
);
const service = module.get<AccountsService>(AccountsService);
await expect(
service.cancelIssue(
makeContext('trackingId'),
tier1Accounts[0].users[0].external_id,
poNumber,
tier5Accounts.account.id,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010811'), HttpStatus.BAD_REQUEST),
);
});
it('自身のパートナー以外の発行をキャンセルしようとした場合、エラー', async () => {
const module = await makeTestingModule(source);
const { tier1Accounts: tier1Accounts } = await makeHierarchicalAccounts(
source,
);
const tier5Accounts = await makeTestAccount(source, {
parent_account_id: 100,
tier: 5,
});
const poNumber = 'CANCEL_TEST';
const date = new Date();
date.setDate(date.getDate() - 14);
await createOrder(
source,
poNumber,
tier5Accounts.account.id,
tier5Accounts.account.parent_account_id,
date,
1,
LICENSE_ISSUE_STATUS.ISSUED,
);
await createLicense(
source,
1,
date,
tier5Accounts.account.id,
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
1,
null,
null,
);
const service = module.get<AccountsService>(AccountsService);
await expect(
service.cancelIssue(
makeContext('trackingId'),
tier1Accounts[0].users[0].external_id,
poNumber,
tier5Accounts.account.id,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E000108'), HttpStatus.UNAUTHORIZED),
);
});
});

View File

@ -40,6 +40,9 @@ import {
LicensesShortageError,
AlreadyIssuedError,
OrderNotFoundError,
AlreadyLicenseStatusChangedError,
AlreadyLicenseAllocatedError,
CancellationPeriodExpiredError,
} from '../../repositories/licenses/errors/types';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import {
@ -1057,6 +1060,90 @@ export class AccountsService {
}
}
/**
*
* @param context
* @param extarnalId
* @param poNumber
* @param orderedAccountId
*/
async cancelIssue(
context: Context,
extarnalId: string,
poNumber: string,
orderedAccountId: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.cancelIssue.name} | params: { ` +
`extarnalId: ${extarnalId}, ` +
`poNumber: ${poNumber}, ` +
`orderedAccountId: ${orderedAccountId}, };`,
);
let myAccountId: number;
try {
// ユーザIDからアカウントIDを取得する
myAccountId = (
await this.usersRepository.findUserByExternalId(extarnalId)
).account_id;
} catch (e) {
this.logger.error(`error=${e}`);
switch (e.constructor) {
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancelIssue.name}`);
}
// 注文元アカウントIDの親世代を取得
const parentAccountIds = await this.accountRepository.getHierarchyParents(
orderedAccountId,
);
// 自身が存在しない場合、エラー
if (!parentAccountIds.includes(myAccountId)) {
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancelIssue.name}`);
throw new HttpException(
makeErrorResponse('E000108'),
HttpStatus.UNAUTHORIZED,
);
}
try {
// 発行キャンセル処理
await this.accountRepository.cancelIssue(orderedAccountId, poNumber);
} catch (e) {
this.logger.error(`error=${e}`);
switch (e.constructor) {
case AlreadyLicenseStatusChangedError:
throw new HttpException(
makeErrorResponse('E010809'),
HttpStatus.BAD_REQUEST,
);
case CancellationPeriodExpiredError:
throw new HttpException(
makeErrorResponse('E010810'),
HttpStatus.BAD_REQUEST,
);
case AlreadyLicenseAllocatedError:
throw new HttpException(
makeErrorResponse('E010811'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancelIssue.name}`);
}
}
/**
*
* @param context

View File

@ -20,17 +20,26 @@ export const getSortCriteria = async (dataSource: DataSource) => {
export const createLicense = async (
datasource: DataSource,
licenseId: number,
expiry_date: Date,
accountId: number,
type: string,
status: string,
allocated_user_id: number,
order_id: number,
deleted_at: Date,
delete_order_id: number,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({
expiry_date: null,
id: licenseId,
expiry_date: expiry_date,
account_id: accountId,
type: 'NORMAL',
status: 'Unallocated',
allocated_user_id: null,
order_id: null,
deleted_at: null,
delete_order_id: null,
type: type,
status: status,
allocated_user_id: allocated_user_id,
order_id: order_id,
deleted_at: deleted_at,
delete_order_id: delete_order_id,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',

View File

@ -346,6 +346,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
await createCardLicense(source, license_id, issueId, cardLicenseKey);
await createCardLicenseIssue(source, issueId);
@ -386,6 +389,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
// 2件目、expiry_dateがnull(OneYear)
await createLicense(
@ -396,6 +402,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
// 3件目、1件目と同じ有効期限
await createLicense(
@ -406,6 +415,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
// 4件目、expiry_dateが一番遠いデータ
await createLicense(
@ -416,6 +428,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
// 5件目、expiry_dateがnull(OneYear)
await createLicense(
@ -426,6 +441,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
// 6件目、ライセンス状態が割当済
await createLicense(
@ -436,6 +454,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
null,
null,
null,
null,
);
// 7件目、ライセンス状態が削除済
await createLicense(
@ -446,6 +467,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.DELETED,
null,
null,
null,
null,
);
// 8件目、別アカウントの未割当のライセンス
await createLicense(
@ -456,6 +480,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
// 9件目、有効期限切れのライセンス
await createLicense(
@ -466,6 +493,9 @@ describe('DBテスト', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
const service = module.get<LicensesService>(LicensesService);
const context = makeContext('userId');
@ -518,6 +548,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
@ -570,6 +603,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.REUSABLE,
null,
null,
null,
null,
);
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
@ -616,6 +652,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId,
null,
null,
null,
);
await createLicense(
source,
@ -625,6 +664,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
@ -697,6 +739,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId,
null,
null,
null,
);
await createLicense(
source,
@ -706,6 +751,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
@ -742,6 +790,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId,
null,
null,
null,
);
await createLicense(
source,
@ -751,6 +802,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
await createLicenseAllocationHistory(source, 1, userId, 1, 'CARD');
@ -787,6 +841,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.TRIAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId,
null,
null,
null,
);
await createLicense(
source,
@ -796,6 +853,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null,
);
await createLicenseAllocationHistory(source, 1, userId, 1, 'TRIAL');
@ -832,6 +892,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.REUSABLE,
null,
null,
null,
null,
);
const service = module.get<UsersService>(UsersService);
@ -863,6 +926,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
null,
null,
null,
null,
);
await createLicense(
source,
@ -872,6 +938,9 @@ describe('ライセンス割り当て', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.DELETED,
null,
null,
null,
null,
);
const service = module.get<UsersService>(UsersService);
@ -927,6 +996,9 @@ describe('ライセンス割り当て解除', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
userId,
null,
null,
null,
);
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
@ -987,6 +1059,9 @@ describe('ライセンス割り当て解除', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
2,
null,
null,
null,
);
await createLicense(
source,
@ -996,6 +1071,9 @@ describe('ライセンス割り当て解除', () => {
LICENSE_TYPE.NORMAL,
LICENSE_ALLOCATED_STATUS.REUSABLE,
userId,
null,
null,
null,
);
await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE');
@ -1037,6 +1115,7 @@ describe('ライセンス注文キャンセル', () => {
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
null,
10,
'Issue Requesting',
);
@ -1046,6 +1125,7 @@ describe('ライセンス注文キャンセル', () => {
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
null,
10,
'Order Canceled',
);
@ -1078,6 +1158,7 @@ describe('ライセンス注文キャンセル', () => {
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
null,
10,
'Issued',
);
@ -1106,6 +1187,7 @@ describe('ライセンス注文キャンセル', () => {
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
null,
10,
'Order Canceled',
);

View File

@ -15,6 +15,9 @@ export const createLicense = async (
type: string,
status: string,
allocated_user_id: number,
order_id: number,
deleted_at: Date,
delete_order_id: number,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(License).insert({
id: licenseId,
@ -23,9 +26,9 @@ export const createLicense = async (
type: type,
status: status,
allocated_user_id: allocated_user_id,
order_id: null,
deleted_at: null,
delete_order_id: null,
order_id: order_id,
deleted_at: deleted_at,
delete_order_id: delete_order_id,
created_by: 'test_runner',
created_at: new Date(),
updated_by: 'updater',
@ -100,6 +103,7 @@ export const createOrder = async (
poNumber: string,
fromId: number,
toId: number,
issuedAt: Date,
quantity: number,
status: string,
): Promise<void> => {
@ -108,7 +112,7 @@ export const createOrder = async (
from_account_id: fromId,
to_account_id: toId,
ordered_at: new Date(),
issued_at: null,
issued_at: issuedAt,
quantity: quantity,
status: status,
canceled_at: null,

View File

@ -20,6 +20,7 @@ import {
} from '../../common/types/sort/util';
import {
LICENSE_ALLOCATED_STATUS,
LICENSE_EXPIRATION_THRESHOLD_DAYS,
LICENSE_ISSUE_STATUS,
TIERS,
} from '../../constants';
@ -28,6 +29,12 @@ import {
PartnerLicenseInfoForRepository,
} from '../../features/accounts/types/types';
import { AccountNotFoundError } from './errors/types';
import {
AlreadyLicenseAllocatedError,
AlreadyLicenseStatusChangedError,
CancellationPeriodExpiredError,
} from '../licenses/errors/types';
import { DateWithZeroTime } from '../../features/licenses/types/types';
@Injectable()
export class AccountsRepositoryService {
@ -572,4 +579,110 @@ export class AccountsRepositoryService {
return accounts;
}
/**
* IDの親世代のアカウントIDをすべて取得する
* (tier)
* @param targetAccountId
* @returns accountIds
*/
async getHierarchyParents(targetAccountId: number): Promise<number[]> {
return await this.dataSource.transaction(async (entityManager) => {
const accountRepository = entityManager.getRepository(Account);
const maxTierDifference = TIERS.TIER5 - TIERS.TIER1;
const parentAccountIds = [];
let currentAccountId = targetAccountId;
// システム的な最大の階層差異分、親を参照する
for (let i = 0; i < maxTierDifference; i++) {
const account = await accountRepository.findOne({
where: {
id: currentAccountId,
},
});
if (!account) {
break;
}
parentAccountIds.push(account.parent_account_id);
currentAccountId = account.parent_account_id;
}
return parentAccountIds;
});
}
/**
* IDとPOナンバーに紐づくライセンス発行をキャンセルする
* @param orderedAccountId:キャンセルしたい発行の注文元アカウントID
* @param poNumber:POナンバー
*/
async cancelIssue(orderedAccountId: number, poNumber: string): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const orderRepo = entityManager.getRepository(LicenseOrder);
// キャンセル対象の発行を取得
const targetOrder = await orderRepo.findOne({
where: {
from_account_id: orderedAccountId,
po_number: poNumber,
status: LICENSE_ISSUE_STATUS.ISSUED,
},
});
// キャンセル対象の発行が存在しない場合エラー
if (!targetOrder) {
throw new AlreadyLicenseStatusChangedError(
`Cancel issue is failed. Already license order status changed. fromAccountId: ${orderedAccountId}, poNumber: ${poNumber}`,
);
}
// キャンセル可能な日付発行日から14日経過かどうかに判定する時刻を取得する
const currentDateWithoutTime = new DateWithZeroTime();
const issuedDateWithoutTime = new DateWithZeroTime(targetOrder.issued_at);
const timeDifference =
currentDateWithoutTime.getTime() - issuedDateWithoutTime.getTime();
const daysDifference = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
// 発行日から14日経過しているかをチェック
if (daysDifference > LICENSE_EXPIRATION_THRESHOLD_DAYS) {
throw new CancellationPeriodExpiredError(
`Cancel issue is failed. Cancellation period expired. fromAccountId: ${orderedAccountId}, poNumber: ${poNumber}`,
);
}
// すでに割り当て済みライセンスを含む注文か確認する
const licenseRepo = entityManager.getRepository(License);
const allocatedLicense = await licenseRepo.findOne({
where: {
order_id: targetOrder.id,
status: Not(LICENSE_ALLOCATED_STATUS.UNALLOCATED),
},
});
// 存在した場合エラー
if (allocatedLicense) {
throw new AlreadyLicenseAllocatedError(
`Cancel issue is failed. Already license allocated. fromAccountId: ${orderedAccountId}, poNumber: ${poNumber}`,
);
}
// 更新用の変数に値をコピー
const updatedOrder = { ...targetOrder };
// 注文を発行待ちに戻す
updatedOrder.issued_at = null;
updatedOrder.status = LICENSE_ISSUE_STATUS.ISSUE_REQUESTING;
await orderRepo.save(updatedOrder);
// 発行時に削除したライセンスを未割当に戻す
await licenseRepo.update(
{ delete_order_id: targetOrder.id },
{
status: LICENSE_ALLOCATED_STATUS.UNALLOCATED,
deleted_at: null,
delete_order_id: null,
},
);
// 発行時に発行されたライセンスを削除する
await licenseRepo.delete({ order_id: targetOrder.id });
});
}
}

View File

@ -24,3 +24,12 @@ export class LicenseAlreadyDeallocatedError extends Error {}
// 注文キャンセル失敗エラー
export class CancelOrderFailedError extends Error {}
// ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
export class AlreadyLicenseStatusChangedError extends Error {}
// ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
export class CancellationPeriodExpiredError extends Error {}
// ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
export class AlreadyLicenseAllocatedError extends Error {}

View File

@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, In, IsNull, MoreThanOrEqual } from 'typeorm';
import { DataSource, In } from 'typeorm';
import {
LicenseOrder,
License,