From 7524abbae6e16a4a6e304f23d66d92e90e2e5b6e Mon Sep 17 00:00:00 2001 From: "maruyama.t" Date: Tue, 5 Sep 2023 05:17:47 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20378:=20API=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=EF=BC=88=E3=83=A9=E3=82=A4=E3=82=BB=E3=83=B3=E3=82=B9=E7=99=BA?= =?UTF-8?q?=E8=A1=8C=E3=82=AD=E3=83=A3=E3=83=B3=E3=82=BB=E3=83=ABAPI?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [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日より経過していた場合はエラー キャンセル対象の発行のライセンスが使われていた場合はエラー 自身のパートナー以外の発行をキャンセルしようとした場合、エラー ## 補足 - 相談、参考資料などがあれば --- dictation_server/src/common/error/code.ts | 3 + dictation_server/src/common/error/message.ts | 3 + .../features/accounts/accounts.controller.ts | 12 +- .../accounts/accounts.service.spec.ts | 507 +++++++++++++++++- .../src/features/accounts/accounts.service.ts | 87 +++ .../src/features/accounts/test/utility.ts | 23 +- .../licenses/licenses.service.spec.ts | 82 +++ .../src/features/licenses/test/utility.ts | 12 +- .../accounts/accounts.repository.service.ts | 113 ++++ .../src/repositories/licenses/errors/types.ts | 9 + .../licenses/licenses.repository.service.ts | 2 +- 11 files changed, 817 insertions(+), 36 deletions(-) diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index f8480b0..09d67f4 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -48,6 +48,9 @@ export const ErrorCodes = [ 'E010806', // ライセンス割り当て不可エラー 'E010807', // ライセンス割り当て解除済みエラー 'E010808', // ライセンス注文キャンセル不可エラー + 'E010809', // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合) + 'E010810', // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合) + 'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) 'E010908', // タイピストグループ不在エラー 'E011001', // ワークタイプ重複エラー 'E011002', // ワークタイプ登録上限超過エラー diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index d11aafb..7515d95 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -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', diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 3dd6f6e..289bee3 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -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 {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index d6f5f0e..b8ab69c 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -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、子1:1、子2:2) - 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); 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); + 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); + 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); + 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); + 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); + 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); + await expect( + service.cancelIssue( + makeContext('trackingId'), + tier1Accounts[0].users[0].external_id, + poNumber, + tier5Accounts.account.id, + ), + ).rejects.toEqual( + new HttpException(makeErrorResponse('E000108'), HttpStatus.UNAUTHORIZED), + ); + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index b350a25..1c9e1ca 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -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 { + 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 diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index e110397..58b2ab4 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -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 => { 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', diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts index f14ed0c..44dc8c8 100644 --- a/dictation_server/src/features/licenses/licenses.service.spec.ts +++ b/dictation_server/src/features/licenses/licenses.service.spec.ts @@ -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); 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); @@ -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); @@ -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', ); diff --git a/dictation_server/src/features/licenses/test/utility.ts b/dictation_server/src/features/licenses/test/utility.ts index da636c5..7fe69a5 100644 --- a/dictation_server/src/features/licenses/test/utility.ts +++ b/dictation_server/src/features/licenses/test/utility.ts @@ -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 => { 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 => { @@ -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, diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index 835c0db..cdd882c 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -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 { + 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 { + 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 }); + }); + } } diff --git a/dictation_server/src/repositories/licenses/errors/types.ts b/dictation_server/src/repositories/licenses/errors/types.ts index 904bbfa..552e24b 100644 --- a/dictation_server/src/repositories/licenses/errors/types.ts +++ b/dictation_server/src/repositories/licenses/errors/types.ts @@ -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 {} diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index a581dff..a53dc7f 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -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,