diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 94fa41c..bf06b11 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -42,4 +42,6 @@ export const ErrorCodes = [ 'E010701', // Blobファイル不在エラー 'E010801', // ライセンス不在エラー 'E010802', // ライセンス取り込み済みエラー + 'E010803', // ライセンス発行済みエラー + 'E010804', // ライセンス不足エラー ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index bbc1b40..3339120 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -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', }; diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 70dbc76..3bc4aa0 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -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 {}; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 7ef47f1..3c55958 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -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); + 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); + 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); + 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( diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 8db4527..5d08225 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -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 { + 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 { this.logger.log(`[IN] ${this.getDealers.name}`); diff --git a/dictation_server/src/features/accounts/test/accounts.service.mock.ts b/dictation_server/src/features/accounts/test/accounts.service.mock.ts index 9f964c4..88dec41 100644 --- a/dictation_server/src/features/accounts/test/accounts.service.mock.ts +++ b/dictation_server/src/features/accounts/test/accounts.service.mock.ts @@ -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, []>() @@ -141,6 +143,21 @@ export const makeLicensesRepositoryMock = ( [] >() .mockResolvedValue(getLicenseOrderHistoryInfo), + issueLicense: + issueLicense instanceof Error + ? jest.fn, []>().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, }; }; diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index c570a4b..b9f3d80 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -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 }; +}; \ No newline at end of file diff --git a/dictation_server/src/repositories/licenses/errors/types.ts b/dictation_server/src/repositories/licenses/errors/types.ts index 972f850..ae3ac02 100644 --- a/dictation_server/src/repositories/licenses/errors/types.ts +++ b/dictation_server/src/repositories/licenses/errors/types.ts @@ -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 {} diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 1d085be..0c3c6ce 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 } 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 { + 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); + } + }); + } }