diff --git a/dictation_server/db/migrations/028-create_lisence_allocation_history.sql b/dictation_server/db/migrations/028-create_lisence_allocation_history.sql index 4dea86c..ccacee8 100644 --- a/dictation_server/db/migrations/028-create_lisence_allocation_history.sql +++ b/dictation_server/db/migrations/028-create_lisence_allocation_history.sql @@ -1,8 +1,9 @@ -- +migrate Up -CREATE TABLE IF NOT EXISTS `lisence_allocation_history` ( - `user_id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'ユーザーID', +CREATE TABLE IF NOT EXISTS `license_allocation_history` ( + `id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT '割り当て履歴ID', + `user_id` BIGINT UNSIGNED NOT NULL COMMENT 'ユーザーID', `license_id` BIGINT UNSIGNED NOT NULL COMMENT 'ライセンスID', - `allocate_type` VARCHAR(255) NOT NULL COMMENT '割り当て種別(割当解除/割当)', + `is_allocated` BOOLEAN NOT NULL DEFAULT 0 COMMENT '割り当て済みか', `executed_at` TIMESTAMP NOT NULL COMMENT '実施日時', `switch_from_type` VARCHAR(255) NOT NULL COMMENT '切り替え元種別(特になし/カード/トライアル)', `deleted_at` TIMESTAMP COMMENT '削除時刻', @@ -13,4 +14,4 @@ CREATE TABLE IF NOT EXISTS `lisence_allocation_history` ( ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; -- +migrate Down -DROP TABLE `lisence_allocation_history`; \ No newline at end of file +DROP TABLE `license_allocation_history`; \ No newline at end of file diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index a42d017..cdf2352 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -119,6 +119,15 @@ export const LICENSE_ALLOCATED_STATUS = { REUSABLE: 'Reusable', DELETED: 'Deleted', } as const; +/** + * 切り替え元種別 + * @const {string[]} + */ +export const SWITCH_FROM_TYPE = { + NONE: 'NONE', + CARD: 'CARD', + TRIAL: 'TRIAL', +} as const; /** * ライセンスの期限切れが近いと見なす日数のしきい値 diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts index 644857c..2e3e46c 100644 --- a/dictation_server/src/features/licenses/licenses.service.spec.ts +++ b/dictation_server/src/features/licenses/licenses.service.spec.ts @@ -28,12 +28,16 @@ import { createCardLicense, createLicense, createCardLicenseIssue, + createLicenseAllocationHistory, selectCardLicensesCount, selectCardLicense, selectLicense, + selectLicenseAllocationHistory, } from './test/utility'; import { UsersService } from '../users/users.service'; import { makeContext } from '../../common/log'; +import { IsNotIn } from 'class-validator'; +import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../constants'; describe('LicensesService', () => { it('ライセンス注文が完了する', async () => { @@ -327,18 +331,19 @@ describe('DBテスト', () => { const cardLicenseKey = 'WZCETXC0Z9PQZ9GKRGGY'; const defaultAccountId = 150; - const licenseId = 50; + const license_id = 50; const issueId = 100; await createLicense( source, - licenseId, + license_id, null, defaultAccountId, - 'Unallocated', + LICENSE_TYPE.CARD, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, null, ); - await createCardLicense(source, licenseId, issueId, cardLicenseKey); + await createCardLicense(source, license_id, issueId, cardLicenseKey); await createCardLicenseIssue(source, issueId); const service = module.get(LicensesService); @@ -348,7 +353,7 @@ describe('DBテスト', () => { source, cardLicenseKey, ); - const dbSelectResultFromLicense = await selectLicense(source, licenseId); + const dbSelectResultFromLicense = await selectLicense(source, license_id); expect( dbSelectResultFromCardLicense.cardLicense.activated_at, ).toBeDefined(); @@ -379,19 +384,44 @@ describe('ライセンス割り当て', () => { const { accountId } = await createAccount(source); const { userId } = await createUser(source, accountId, 'userId', 'admin'); - await createLicense(source, 1, null, accountId, 'Unallocated', null); + await createLicense( + source, + 1, + null, + accountId, + LICENSE_TYPE.CARD, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + ); + await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE'); const service = module.get(UsersService); const expiry_date = new NewAllocatedLicenseExpirationDate(); await service.allocateLicense(makeContext('trackingId'), userId, 1); - const result = await selectLicense(source, 1); - expect(result.license.allocated_user_id).toBe(userId); - expect(result.license.status).toBe('Allocated'); - expect(result.license.expiry_date.setMilliseconds(0)).toEqual( + const resultLicense = await selectLicense(source, 1); + expect(resultLicense.license.allocated_user_id).toBe(userId); + expect(resultLicense.license.status).toBe( + LICENSE_ALLOCATED_STATUS.ALLOCATED, + ); + expect(resultLicense.license.expiry_date.setMilliseconds(0)).toEqual( expiry_date.setMilliseconds(0), ); + const licenseAllocationHistory = await selectLicenseAllocationHistory( + source, + userId, + 1, + ); + expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe( + userId, + ); + expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe( + 1, + ); + expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe( + true, + ); }); it('再割り当て可能なライセンスに対して、ライセンス割り当てが完了する', async () => { @@ -401,15 +431,38 @@ describe('ライセンス割り当て', () => { const { userId } = await createUser(source, accountId, 'userId', 'admin'); const date = new Date(); date.setDate(date.getDate() + 30); - await createLicense(source, 1, date, accountId, 'Reusable', null); + await createLicense( + source, + 1, + date, + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + ); + await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE'); const service = module.get(UsersService); await service.allocateLicense(makeContext('trackingId'), userId, 1); const result = await selectLicense(source, 1); expect(result.license.allocated_user_id).toBe(userId); - expect(result.license.status).toBe('Allocated'); + expect(result.license.status).toBe(LICENSE_ALLOCATED_STATUS.ALLOCATED); expect(result.license.expiry_date).toEqual(date); + const licenseAllocationHistory = await selectLicenseAllocationHistory( + source, + userId, + 1, + ); + expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe( + userId, + ); + expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe( + 1, + ); + expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe( + true, + ); }); it('未割当のライセンスに対して、別のライセンスが割り当てられているユーザーの割り当てが完了する', async () => { @@ -419,8 +472,25 @@ describe('ライセンス割り当て', () => { const { userId } = await createUser(source, accountId, 'userId', 'admin'); const date = new Date(); date.setDate(date.getDate() + 30); - await createLicense(source, 1, date, accountId, 'Allocated', userId); - await createLicense(source, 2, null, accountId, 'Unallocated', null); + await createLicense( + source, + 1, + date, + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + userId, + ); + await createLicense( + source, + 2, + null, + accountId, + LICENSE_TYPE.CARD, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + ); + await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE'); const service = module.get(UsersService); @@ -431,16 +501,164 @@ describe('ライセンス割り当て', () => { // もともと割り当てられていたライセンスの状態確認 const result1 = await selectLicense(source, 1); expect(result1.license.allocated_user_id).toBe(null); - expect(result1.license.status).toBe('Reusable'); + expect(result1.license.status).toBe(LICENSE_ALLOCATED_STATUS.REUSABLE); expect(result1.license.expiry_date).toEqual(date); + const licenseAllocationHistory = await selectLicenseAllocationHistory( + source, + userId, + 1, + ); + expect(licenseAllocationHistory.licenseAllocationHistory.user_id).toBe( + userId, + ); + expect(licenseAllocationHistory.licenseAllocationHistory.license_id).toBe( + 1, + ); + expect(licenseAllocationHistory.licenseAllocationHistory.is_allocated).toBe( + false, + ); // 新たに割り当てたライセンスの状態確認 const result2 = await selectLicense(source, 2); expect(result2.license.allocated_user_id).toBe(userId); - expect(result2.license.status).toBe('Allocated'); + expect(result2.license.status).toBe(LICENSE_ALLOCATED_STATUS.ALLOCATED); expect(result2.license.expiry_date.setMilliseconds(0)).toEqual( expiry_date.setMilliseconds(0), ); + const newlicenseAllocationHistory = await selectLicenseAllocationHistory( + source, + userId, + 2, + ); + expect(newlicenseAllocationHistory.licenseAllocationHistory.user_id).toBe( + userId, + ); + expect( + newlicenseAllocationHistory.licenseAllocationHistory.license_id, + ).toBe(2); + expect( + newlicenseAllocationHistory.licenseAllocationHistory.is_allocated, + ).toBe(true); + }); + + it('割り当て時にライセンス履歴テーブルへの登録が完了する(元がNORMALのとき)', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { userId } = await createUser(source, accountId, 'userId', 'admin'); + const date = new Date(); + date.setDate(date.getDate() + 30); + await createLicense( + source, + 1, + date, + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + userId, + ); + await createLicense( + source, + 2, + null, + accountId, + LICENSE_TYPE.CARD, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + ); + await createLicenseAllocationHistory(source, 1, userId, 1, 'NONE'); + + const service = module.get(UsersService); + await service.allocateLicense(makeContext('trackingId'), userId, 2); + + const licenseAllocationHistory = await selectLicenseAllocationHistory( + source, + userId, + 2, + ); + expect( + licenseAllocationHistory.licenseAllocationHistory.switch_from_type, + ).toBe('NONE'); + }); + + it('割り当て時にライセンス履歴テーブルへの登録が完了する(元がCARDのとき)', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { userId } = await createUser(source, accountId, 'userId', 'admin'); + const date = new Date(); + date.setDate(date.getDate() + 30); + await createLicense( + source, + 1, + date, + accountId, + LICENSE_TYPE.CARD, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + userId, + ); + await createLicense( + source, + 2, + null, + accountId, + LICENSE_TYPE.CARD, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + ); + await createLicenseAllocationHistory(source, 1, userId, 1, 'CARD'); + + const service = module.get(UsersService); + await service.allocateLicense(makeContext('trackingId'), userId, 2); + + const licenseAllocationHistory = await selectLicenseAllocationHistory( + source, + userId, + 2, + ); + expect( + licenseAllocationHistory.licenseAllocationHistory.switch_from_type, + ).toBe('CARD'); + }); + + it('割り当て時にライセンス履歴テーブルへの登録が完了する(元がTRIALのとき)', async () => { + const module = await makeTestingModule(source); + + const { accountId } = await createAccount(source); + const { userId } = await createUser(source, accountId, 'userId', 'admin'); + const date = new Date(); + date.setDate(date.getDate() + 30); + await createLicense( + source, + 1, + date, + accountId, + LICENSE_TYPE.TRIAL, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + userId, + ); + await createLicense( + source, + 2, + null, + accountId, + LICENSE_TYPE.CARD, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + ); + await createLicenseAllocationHistory(source, 1, userId, 1, 'TRIAL'); + + const service = module.get(UsersService); + await service.allocateLicense(makeContext('trackingId'), userId, 2); + + const licenseAllocationHistory = await selectLicenseAllocationHistory( + source, + userId, + 2, + ); + expect( + licenseAllocationHistory.licenseAllocationHistory.switch_from_type, + ).toBe('TRIAL'); }); it('有効期限が切れているライセンスを割り当てようとした場合、エラーになる', async () => { @@ -450,7 +668,15 @@ describe('ライセンス割り当て', () => { const { userId } = await createUser(source, accountId, 'userId', 'admin'); const date = new Date(); date.setDate(date.getDate() - 30); - await createLicense(source, 1, date, accountId, 'Reusable', null); + await createLicense( + source, + 1, + date, + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + ); const service = module.get(UsersService); @@ -468,8 +694,24 @@ describe('ライセンス割り当て', () => { const { userId } = await createUser(source, accountId, 'userId', 'admin'); const date = new Date(); date.setDate(date.getDate() + 30); - await createLicense(source, 1, null, accountId, 'Allocated', null); - await createLicense(source, 2, null, accountId, 'Deleted', null); + await createLicense( + source, + 1, + null, + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + ); + await createLicense( + source, + 2, + null, + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.DELETED, + null, + ); const service = module.get(UsersService); diff --git a/dictation_server/src/features/licenses/test/utility.ts b/dictation_server/src/features/licenses/test/utility.ts index c7bfb80..352c974 100644 --- a/dictation_server/src/features/licenses/test/utility.ts +++ b/dictation_server/src/features/licenses/test/utility.ts @@ -5,6 +5,7 @@ import { License, CardLicense, CardLicenseIssue, + LicenseAllocationHistory, } from '../../../repositories/licenses/entity/license.entity'; export const createAccount = async ( @@ -60,6 +61,7 @@ export const createLicense = async ( licenseId: number, expiry_date: Date, accountId: number, + type: string, status: string, allocated_user_id: number, ): Promise => { @@ -67,7 +69,7 @@ export const createLicense = async ( id: licenseId, expiry_date: expiry_date, account_id: accountId, - type: 'card', + type: type, status: status, allocated_user_id: allocated_user_id, order_id: null, @@ -117,6 +119,31 @@ export const createCardLicenseIssue = async ( identifiers.pop() as CardLicenseIssue; }; +export const createLicenseAllocationHistory = async ( + datasource: DataSource, + historyId: number, + userId: number, + licenseId: number, + type: string, +): Promise => { + const { identifiers } = await datasource + .getRepository(LicenseAllocationHistory) + .insert({ + id: historyId, + user_id: userId, + license_id: licenseId, + is_allocated: true, + executed_at: new Date(), + switch_from_type: type, + deleted_at: null, + created_by: null, + created_at: new Date(), + updated_by: null, + updated_at: new Date(), + }); + identifiers.pop() as LicenseAllocationHistory; +}; + export const selectCardLicensesCount = async ( datasource: DataSource, ): Promise<{ count: number }> => { @@ -147,3 +174,22 @@ export const selectLicense = async ( }); return { license }; }; + +export const selectLicenseAllocationHistory = async ( + datasource: DataSource, + userId: number, + licence_id: number, +): Promise<{ licenseAllocationHistory: LicenseAllocationHistory }> => { + const licenseAllocationHistory = await datasource + .getRepository(LicenseAllocationHistory) + .findOne({ + where: { + user_id: userId, + license_id: licence_id, + }, + order: { + executed_at: 'DESC', + }, + }); + return { licenseAllocationHistory }; +}; diff --git a/dictation_server/src/repositories/licenses/entity/license.entity.ts b/dictation_server/src/repositories/licenses/entity/license.entity.ts index 301b0ff..3d3df06 100644 --- a/dictation_server/src/repositories/licenses/entity/license.entity.ts +++ b/dictation_server/src/repositories/licenses/entity/license.entity.ts @@ -6,6 +6,7 @@ import { UpdateDateColumn, OneToOne, JoinColumn, + ManyToOne, } from 'typeorm'; import { User } from '../../users/entity/user.entity'; @@ -164,3 +165,43 @@ export class CardLicense { @UpdateDateColumn({}) updated_at: Date; } + +@Entity({ name: 'license_allocation_history' }) +export class LicenseAllocationHistory { + @PrimaryGeneratedColumn() + id: number; + + @Column() + user_id: number; + + @Column() + license_id: number; + + @Column() + is_allocated: boolean; + + @Column() + executed_at: Date; + + @Column() + switch_from_type: string; + + @Column({ nullable: true }) + deleted_at: Date; + + @Column({ nullable: true }) + created_by: string; + + @CreateDateColumn() + created_at: Date; + + @Column({ nullable: true }) + updated_by: string; + + @UpdateDateColumn() + updated_at: Date; + + @ManyToOne(() => License, (licenses) => licenses.id) + @JoinColumn({ name: 'license_id' }) + license?: License; +} diff --git a/dictation_server/src/repositories/licenses/licenses.repository.module.ts b/dictation_server/src/repositories/licenses/licenses.repository.module.ts index a142c1f..252f01b 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.module.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.module.ts @@ -5,6 +5,7 @@ import { CardLicenseIssue, License, LicenseOrder, + LicenseAllocationHistory, } from './entity/license.entity'; import { LicensesRepositoryService } from './licenses.repository.service'; @@ -15,6 +16,7 @@ import { LicensesRepositoryService } from './licenses.repository.service'; License, CardLicense, CardLicenseIssue, + LicenseAllocationHistory, ]), ], providers: [LicensesRepositoryService], diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index f624405..694d48a 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -5,6 +5,7 @@ import { License, CardLicenseIssue, CardLicense, + LicenseAllocationHistory, } from './entity/license.entity'; import { CARD_LICENSE_LENGTH, @@ -12,6 +13,7 @@ import { LICENSE_STATUS_ISSUE_REQUESTING, LICENSE_STATUS_ISSUED, LICENSE_TYPE, + SWITCH_FROM_TYPE, TIERS, } from '../../constants'; import { @@ -400,8 +402,11 @@ export class LicensesRepositoryService { */ async allocateLicense(userId: number, newLicenseId: number): Promise { await this.dataSource.transaction(async (entityManager) => { - // 割り当て対象のライセンス情報を取得 const licenseRepo = entityManager.getRepository(License); + const licenseAllocationHistoryRepo = entityManager.getRepository( + LicenseAllocationHistory, + ); + // 割り当て対象のライセンス情報を取得 const targetLicense = await licenseRepo.findOne({ where: { id: newLicenseId, @@ -439,7 +444,17 @@ export class LicensesRepositoryService { if (allocatedLicense) { allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE; allocatedLicense.allocated_user_id = null; + await licenseRepo.save(allocatedLicense); + + // ライセンス割り当て履歴テーブルへ登録 + const deallocationHistory = new LicenseAllocationHistory(); + deallocationHistory.user_id = userId; + deallocationHistory.license_id = allocatedLicense.id; + deallocationHistory.is_allocated = false; + deallocationHistory.executed_at = new Date(); + deallocationHistory.switch_from_type = SWITCH_FROM_TYPE.NONE; + await licenseAllocationHistoryRepo.save(deallocationHistory); } // ライセンス割り当てを実施 @@ -450,6 +465,39 @@ export class LicensesRepositoryService { targetLicense.expiry_date = new NewAllocatedLicenseExpirationDate(); } await licenseRepo.save(targetLicense); + + // 直近割り当てたライセンス種別を取得 + const oldLicenseType = await licenseAllocationHistoryRepo.findOne({ + relations: { + license: true, + }, + where: { user_id: userId, is_allocated: true }, + order: { executed_at: 'DESC' }, + }); + + let switchFromType = ''; + switch (oldLicenseType.license.type) { + case LICENSE_TYPE.CARD: + switchFromType = SWITCH_FROM_TYPE.CARD; + break; + case LICENSE_TYPE.TRIAL: + switchFromType = SWITCH_FROM_TYPE.TRIAL; + break; + default: + switchFromType = SWITCH_FROM_TYPE.NONE; + break; + } + + // ライセンス割り当て履歴テーブルへ登録 + const allocationHistory = new LicenseAllocationHistory(); + allocationHistory.user_id = userId; + allocationHistory.license_id = targetLicense.id; + allocationHistory.is_allocated = true; + allocationHistory.executed_at = new Date(); + // TODO switchFromTypeの値については「PBI1234: 第一階層として、ライセンス数推移情報をCSV出力したい」で正式対応 + allocationHistory.switch_from_type = switchFromType; + + await licenseAllocationHistoryRepo.save(allocationHistory); }); } }