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:
parent
6b91745b2b
commit
cb7ba77bc3
@ -47,5 +47,6 @@ export const ErrorCodes = [
|
||||
'E010805', // ライセンス有効期限切れエラー
|
||||
'E010806', // ライセンス割り当て不可エラー
|
||||
'E010807', // ライセンス割り当て解除済みエラー
|
||||
'E010808', // ライセンス注文キャンセル不可エラー
|
||||
'E010908', // タイピストグループ不在エラー
|
||||
] as const;
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
/**
|
||||
* ライセンス種別
|
||||
|
||||
@ -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 {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -21,3 +21,6 @@ export class LicenseUnavailableError extends Error {}
|
||||
|
||||
// ライセンス割り当て解除済みエラー
|
||||
export class LicenseAlreadyDeallocatedError extends Error {}
|
||||
|
||||
// 注文キャンセル失敗エラー
|
||||
export class CancelOrderFailedError extends Error {}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user