Merged PR 362: API実装(ライセンス注文キャンセルAPI)

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

ライセンス注文キャンセルAPIを実装しました。

## レビューポイント
なし

## UIの変更
なし

## 動作確認状況
ローカルでUT、動作確認実施済み

## 補足
なし
This commit is contained in:
oura.a 2023-08-31 00:25:13 +00:00
parent 6b91745b2b
commit cb7ba77bc3
10 changed files with 274 additions and 27 deletions

View File

@ -47,5 +47,6 @@ export const ErrorCodes = [
'E010805', // ライセンス有効期限切れエラー
'E010806', // ライセンス割り当て不可エラー
'E010807', // ライセンス割り当て解除済みエラー
'E010808', // ライセンス注文キャンセル不可エラー
'E010908', // タイピストグループ不在エラー
] as const;

View File

@ -36,5 +36,6 @@ export const errors: Errors = {
E010805: 'License is expired Error',
E010806: 'License is unavailable Error',
E010807: 'License is already deallocated Error',
E010808: 'Order cancel failed Error',
E010908: 'Typist Group not exist Error',
};

View File

@ -89,16 +89,14 @@ export const USER_ROLES = {
} as const;
/**
*
* @const {string}
*
* @const {string[]}
*/
export const LICENSE_STATUS_ISSUE_REQUESTING = 'Issue Requesting';
/**
*
* @const {string}
*/
export const LICENSE_STATUS_ISSUED = 'Issued';
export const LICENSE_ISSUE_STATUS = {
ISSUE_REQUESTING: 'Issue Requesting',
ISSUED: 'Issued',
CANCELED: 'Order Canceled',
};
/**
*

View File

@ -250,11 +250,11 @@ export class LicensesController {
const context = makeContext(payload.userId);
// 注文キャンセル処理(仮)
// await this.licensesService.cancelOrder(
// context,
// body.poNumber,
// );
await this.licensesService.cancelOrder(
context,
payload.userId,
body.poNumber,
);
return {};
}
}

View File

@ -31,11 +31,17 @@ import {
selectCardLicense,
selectLicense,
selectLicenseAllocationHistory,
createOrder,
selectOrderLicense,
} from './test/utility';
import { UsersService } from '../users/users.service';
import { makeContext } from '../../common/log';
import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants';
import { makeTestSimpleAccount, makeTestUser } from '../../common/test/utility';
import {
makeHierarchicalAccounts,
makeTestSimpleAccount,
makeTestUser,
} from '../../common/test/utility';
describe('LicensesService', () => {
it('ライセンス注文が完了する', async () => {
@ -1001,3 +1007,118 @@ 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 { tier2Accounts: tier2Accounts } = await makeHierarchicalAccounts(
source,
);
const poNumber = 'CANCEL_TEST';
await createOrder(
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
10,
'Issue Requesting',
);
// キャンセル済みの同名poNumoberが存在しても正常に動作することの確認用order
await createOrder(
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
10,
'Order Canceled',
);
const service = module.get<LicensesService>(LicensesService);
await service.cancelOrder(
makeContext('trackingId'),
tier2Accounts[0].users[0].external_id,
poNumber,
);
// 割り当て解除したライセンスの状態確認
const orderRecord = await selectOrderLicense(
source,
tier2Accounts[0].account.id,
poNumber,
);
expect(orderRecord.orderLicense.canceled_at).toBeDefined();
expect(orderRecord.orderLicense.status).toBe('Order Canceled');
});
it('ライセンスが既に発行済みの場合、エラーとなる', async () => {
const module = await makeTestingModule(source);
const { tier2Accounts: tier2Accounts } = await makeHierarchicalAccounts(
source,
);
const poNumber = 'CANCEL_TEST';
await createOrder(
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
10,
'Issued',
);
const service = module.get<LicensesService>(LicensesService);
await expect(
service.cancelOrder(
makeContext('trackingId'),
tier2Accounts[0].users[0].external_id,
poNumber,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010808'), HttpStatus.BAD_REQUEST),
);
});
it('ライセンスが既にキャンセル済みの場合、エラーとなる', async () => {
const module = await makeTestingModule(source);
const { tier2Accounts: tier2Accounts } = await makeHierarchicalAccounts(
source,
);
const poNumber = 'CANCEL_TEST';
await createOrder(
source,
poNumber,
tier2Accounts[0].account.id,
tier2Accounts[0].account.parent_account_id,
10,
'Order Canceled',
);
const service = module.get<LicensesService>(LicensesService);
await expect(
service.cancelOrder(
makeContext('trackingId'),
tier2Accounts[0].users[0].external_id,
poNumber,
),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010808'), HttpStatus.BAD_REQUEST),
);
});
});

View File

@ -8,6 +8,7 @@ import {
PoNumberAlreadyExistError,
LicenseNotExistError,
LicenseKeyAlreadyActivatedError,
CancelOrderFailedError,
} from '../../repositories/licenses/errors/types';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service';
import { UserNotFoundError } from '../../repositories/users/errors/types';
@ -255,4 +256,54 @@ export class LicensesService {
);
}
}
/**
*
* @param context
* @param externalId
* @param poNumber
*/
async cancelOrder(
context: Context,
externalId: string,
poNumber: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.trackingId}] ${this.cancelOrder.name} | params: { ` +
`externalId: ${externalId}, ` +
`poNumber: ${poNumber}, };`,
);
let myAccountId: number;
try {
// ユーザIDからアカウントIDを取得する
myAccountId = (
await this.usersRepository.findUserByExternalId(externalId)
).account_id;
// 注文キャンセル処理
await this.licensesRepository.cancelOrder(myAccountId, poNumber);
} catch (e) {
this.logger.error(`error=${e}`);
switch (e.constructor) {
case UserNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case CancelOrderFailedError:
throw new HttpException(
makeErrorResponse('E010808'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
} finally {
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancelOrder.name}`);
}
return;
}
}

View File

@ -4,6 +4,7 @@ import {
CardLicense,
CardLicenseIssue,
LicenseAllocationHistory,
LicenseOrder,
} from '../../../repositories/licenses/entity/license.entity';
export const createLicense = async (
@ -94,6 +95,31 @@ export const createLicenseAllocationHistory = async (
identifiers.pop() as LicenseAllocationHistory;
};
export const createOrder = async (
datasource: DataSource,
poNumber: string,
fromId: number,
toId: number,
quantity: number,
status: string,
): Promise<void> => {
const { identifiers } = await datasource.getRepository(LicenseOrder).insert({
po_number: poNumber,
from_account_id: fromId,
to_account_id: toId,
ordered_at: new Date(),
issued_at: null,
quantity: quantity,
status: status,
canceled_at: null,
created_by: null,
created_at: new Date(),
updated_by: null,
updated_at: new Date(),
});
identifiers.pop() as LicenseOrder;
};
export const selectCardLicensesCount = async (
datasource: DataSource,
): Promise<{ count: number }> => {
@ -143,3 +169,17 @@ export const selectLicenseAllocationHistory = async (
});
return { licenseAllocationHistory };
};
export const selectOrderLicense = async (
datasource: DataSource,
accountId: number,
poNumber: string,
): Promise<{ orderLicense: LicenseOrder }> => {
const orderLicense = await datasource.getRepository(LicenseOrder).findOne({
where: {
from_account_id: accountId,
po_number: poNumber,
},
});
return { orderLicense };
};

View File

@ -20,7 +20,7 @@ import {
} from '../../common/types/sort/util';
import {
LICENSE_ALLOCATED_STATUS,
LICENSE_STATUS_ISSUE_REQUESTING,
LICENSE_ISSUE_STATUS,
TIERS,
} from '../../constants';
import {
@ -330,7 +330,7 @@ export class AccountsRepositoryService {
const numberOfRequesting = await licenseOrder.count({
where: {
from_account_id: id,
status: LICENSE_STATUS_ISSUE_REQUESTING,
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
},
});
@ -340,7 +340,7 @@ export class AccountsRepositoryService {
.select('SUM(license_orders.quantity)', 'sum')
.where('license_orders.from_account_id = :id', { id })
.andWhere('license_orders.status = :status', {
status: LICENSE_STATUS_ISSUE_REQUESTING,
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
})
.getRawOne();
const issueRequesting = parseInt(result.sum, 10) || 0;
@ -415,7 +415,7 @@ export class AccountsRepositoryService {
.select('SUM(license_orders.quantity)', 'sum')
.where('license_orders.to_account_id = :id', { id })
.andWhere('license_orders.status = :status', {
status: LICENSE_STATUS_ISSUE_REQUESTING,
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
})
.getRawOne();
const issuedRequested = parseInt(issuedRequestedSqlResult.sum, 10) || 0;
@ -426,7 +426,7 @@ export class AccountsRepositoryService {
.select('SUM(license_orders.quantity)', 'sum')
.where('license_orders.from_account_id = :id', { id })
.andWhere('license_orders.status = :status', {
status: LICENSE_STATUS_ISSUE_REQUESTING,
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
})
.getRawOne();
const issuedRequesting = parseInt(issuedRequestingSqlResult.sum, 10) || 0;

View File

@ -21,3 +21,6 @@ export class LicenseUnavailableError extends Error {}
// ライセンス割り当て解除済みエラー
export class LicenseAlreadyDeallocatedError extends Error {}
// 注文キャンセル失敗エラー
export class CancelOrderFailedError extends Error {}

View File

@ -10,8 +10,7 @@ import {
import {
CARD_LICENSE_LENGTH,
LICENSE_ALLOCATED_STATUS,
LICENSE_STATUS_ISSUE_REQUESTING,
LICENSE_STATUS_ISSUED,
LICENSE_ISSUE_STATUS,
LICENSE_TYPE,
SWITCH_FROM_TYPE,
TIERS,
@ -26,6 +25,7 @@ import {
LicenseExpiredError,
LicenseUnavailableError,
LicenseAlreadyDeallocatedError,
CancelOrderFailedError,
} from './errors/types';
import {
AllocatableLicenseInfo,
@ -49,7 +49,7 @@ export class LicensesRepositoryService {
licenseOrder.from_account_id = fromAccountId;
licenseOrder.to_account_id = toAccountId;
licenseOrder.quantity = quantity;
licenseOrder.status = LICENSE_STATUS_ISSUE_REQUESTING;
licenseOrder.status = LICENSE_ISSUE_STATUS.ISSUE_REQUESTING;
// ライセンス注文テーブルに登録する
const createdEntity = await this.dataSource.transaction(
@ -62,12 +62,12 @@ export class LicensesRepositoryService {
{
po_number: poNumber,
from_account_id: fromAccountId,
status: LICENSE_STATUS_ISSUED,
status: LICENSE_ISSUE_STATUS.ISSUED,
},
{
po_number: poNumber,
from_account_id: fromAccountId,
status: LICENSE_STATUS_ISSUE_REQUESTING,
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
},
],
});
@ -351,7 +351,7 @@ export class LicensesRepositoryService {
throw new OrderNotFoundError(`No order found for PONumber:${poNumber}`);
}
// 既に発行済みの注文の場合、エラー
if (issuingOrder.status !== LICENSE_STATUS_ISSUE_REQUESTING) {
if (issuingOrder.status !== LICENSE_ISSUE_STATUS.ISSUE_REQUESTING) {
throw new AlreadyIssuedError(
`An order for PONumber:${poNumber} has already been issued.`,
);
@ -371,7 +371,7 @@ export class LicensesRepositoryService {
{ id: issuingOrder.id },
{
issued_at: nowDate,
status: LICENSE_STATUS_ISSUED,
status: LICENSE_ISSUE_STATUS.ISSUED,
},
);
// ライセンステーブルを登録(注文元)
@ -597,4 +597,36 @@ export class LicensesRepositoryService {
await licenseAllocationHistoryRepo.save(deallocationHistory);
});
}
/**
*
* @param accountId
* @param poNumber
*/
async cancelOrder(accountId: 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: accountId,
po_number: poNumber,
status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING,
},
});
// キャンセル対象の注文が存在しない場合エラー
if (!targetOrder) {
throw new CancelOrderFailedError(
`Cancel order is failed. accountId: ${accountId}, poNumber: ${poNumber}`,
);
}
// 注文キャンセル処理
targetOrder.status = LICENSE_ISSUE_STATUS.CANCELED;
targetOrder.canceled_at = new Date();
await orderRepo.save(targetOrder);
});
}
}