Merged PR 631: ライセンス自動割り当て処理実装(メイン処理)

## 概要
[Task3294: ライセンス自動割り当て処理実装(メイン処理)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3294)

ライセンス自動割り当て処理を実装しました。

ラフスケッチでは1回のクエリでアカウント・ユーザーを両方取得する設計でしたが、実装難度・可読性の面から、
アカウントとユーザーを別々に取得するよう変更しています。

## レビューポイント
処理内容に過不足がないか。
DBからのデータ取得時の条件に過不足がないか。

## UIの変更
なし

## 動作確認状況
ローカルでUT,動作確認済み

## 補足
なし
This commit is contained in:
oura.a 2023-12-20 01:24:31 +00:00
parent 9e1bc8944f
commit 32a452bdb2
8 changed files with 1013 additions and 88 deletions

View File

@ -1,6 +1,7 @@
import {
LICENSE_EXPIRATION_DAYS,
LICENSE_EXPIRATION_THRESHOLD_DAYS,
LICENSE_EXPIRATION_TIME_WITH_TIMEZONE,
TRIAL_LICENSE_EXPIRATION_DAYS,
} from "../../constants";
@ -28,6 +29,19 @@ export class DateWithDayEndTime extends Date {
}
}
// 翌日の日付を取得する
export class DateWithNextDayEndTime extends Date {
constructor(...args: any[]) {
if (args.length === 0) {
super(); // 引数がない場合、現在の日付で初期化
} else {
super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す
}
this.setDate(this.getDate() + 1);
this.setHours(23, 59, 59, 999); // 時分秒を"23:59:59.999"に固定
}
}
// ライセンスの算出用に、閾値となる時刻23:59:59.999)の日付を取得する
export class ExpirationThresholdDate extends Date {
constructor(...args: any[]) {
@ -49,6 +63,8 @@ export class NewTrialLicenseExpirationDate extends Date {
} else {
super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す
}
// タイムゾーンをカバーするために現在時刻に8時間を加算してから、30日後の日付を取得する
this.setHours(this.getHours() + LICENSE_EXPIRATION_TIME_WITH_TIMEZONE);
this.setDate(this.getDate() + TRIAL_LICENSE_EXPIRATION_DAYS);
this.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定
this.setMilliseconds(0);
@ -63,6 +79,8 @@ export class NewAllocatedLicenseExpirationDate extends Date {
} else {
super(...(args as [string])); // 引数がある場合、引数をそのままDateクラスのコンストラクタに渡す
}
// タイムゾーンをカバーするために現在時刻に8時間を加算してから、365日後の日付を取得する
this.setHours(this.getHours() + LICENSE_EXPIRATION_TIME_WITH_TIMEZONE);
this.setDate(this.getDate() + LICENSE_EXPIRATION_DAYS);
this.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定
this.setMilliseconds(0);

View File

@ -19,54 +19,54 @@ export const TIERS = {
* East USに保存する国リスト
* @const {number}
*/
export const BLOB_STORAGE_REGION_US = ['CA', 'KY', 'US'];
export const BLOB_STORAGE_REGION_US = ["CA", "KY", "US"];
/**
* Australia Eastに保存する国リスト
* @const {number}
*/
export const BLOB_STORAGE_REGION_AU = ['AU', 'NZ'];
export const BLOB_STORAGE_REGION_AU = ["AU", "NZ"];
/**
* North Europeに保存する国リスト
* @const {number}
*/
export const BLOB_STORAGE_REGION_EU = [
'AT',
'BE',
'BG',
'HR',
'CY',
'CZ',
'DK',
'EE',
'FI',
'FR',
'DE',
'GR',
'HU',
'IS',
'IE',
'IT',
'LV',
'LI',
'LT',
'LU',
'MT',
'NL',
'NO',
'PL',
'PT',
'RO',
'RS',
'SK',
'SI',
'ZA',
'ES',
'SE',
'CH',
'TR',
'GB',
"AT",
"BE",
"BG",
"HR",
"CY",
"CZ",
"DK",
"EE",
"FI",
"FR",
"DE",
"GR",
"HU",
"IS",
"IE",
"IT",
"LV",
"LI",
"LT",
"LU",
"MT",
"NL",
"NO",
"PL",
"PT",
"RO",
"RS",
"SK",
"SI",
"ZA",
"ES",
"SE",
"CH",
"TR",
"GB",
];
/**
@ -74,8 +74,8 @@ export const BLOB_STORAGE_REGION_EU = [
* @const {string[]}
*/
export const ADMIN_ROLES = {
ADMIN: 'admin',
STANDARD: 'standard',
ADMIN: "admin",
STANDARD: "standard",
} as const;
/**
@ -83,9 +83,9 @@ export const ADMIN_ROLES = {
* @const {string[]}
*/
export const USER_ROLES = {
NONE: 'none',
AUTHOR: 'author',
TYPIST: 'typist',
NONE: "none",
AUTHOR: "author",
TYPIST: "typist",
} as const;
/**
@ -93,9 +93,9 @@ export const USER_ROLES = {
* @const {string[]}
*/
export const LICENSE_ISSUE_STATUS = {
ISSUE_REQUESTING: 'Issue Requesting',
ISSUED: 'Issued',
CANCELED: 'Order Canceled',
ISSUE_REQUESTING: "Issue Requesting",
ISSUED: "Issued",
CANCELED: "Order Canceled",
};
/**
@ -103,28 +103,28 @@ export const LICENSE_ISSUE_STATUS = {
* @const {string[]}
*/
export const LICENSE_TYPE = {
TRIAL: 'TRIAL',
NORMAL: 'NORMAL',
CARD: 'CARD',
TRIAL: "TRIAL",
NORMAL: "NORMAL",
CARD: "CARD",
} as const;
/**
*
* @const {string[]}
*/
export const LICENSE_ALLOCATED_STATUS = {
UNALLOCATED: 'Unallocated',
ALLOCATED: 'Allocated',
REUSABLE: 'Reusable',
DELETED: 'Deleted',
UNALLOCATED: "Unallocated",
ALLOCATED: "Allocated",
REUSABLE: "Reusable",
DELETED: "Deleted",
} as const;
/**
*
* @const {string[]}
*/
export const SWITCH_FROM_TYPE = {
NONE: 'NONE',
CARD: 'CARD',
TRIAL: 'TRIAL',
NONE: "NONE",
CARD: "CARD",
TRIAL: "TRIAL",
} as const;
/**
@ -139,6 +139,12 @@ export const LICENSE_EXPIRATION_THRESHOLD_DAYS = 14;
*/
export const LICENSE_EXPIRATION_DAYS = 365;
/**
* 8
* @const {number}
*/
export const LICENSE_EXPIRATION_TIME_WITH_TIMEZONE = 8;
/**
*
* @const {number}
@ -156,36 +162,36 @@ export const OPTION_ITEM_NUM = 10;
* @const {string[]}
*/
export const TASK_STATUS = {
UPLOADED: 'Uploaded',
PENDING: 'Pending',
IN_PROGRESS: 'InProgress',
FINISHED: 'Finished',
BACKUP: 'Backup',
UPLOADED: "Uploaded",
PENDING: "Pending",
IN_PROGRESS: "InProgress",
FINISHED: "Finished",
BACKUP: "Backup",
} as const;
/**
*
*/
export const TASK_LIST_SORTABLE_ATTRIBUTES = [
'JOB_NUMBER',
'STATUS',
'ENCRYPTION',
'AUTHOR_ID',
'WORK_TYPE',
'FILE_NAME',
'FILE_LENGTH',
'FILE_SIZE',
'RECORDING_STARTED_DATE',
'RECORDING_FINISHED_DATE',
'UPLOAD_DATE',
'TRANSCRIPTION_STARTED_DATE',
'TRANSCRIPTION_FINISHED_DATE',
"JOB_NUMBER",
"STATUS",
"ENCRYPTION",
"AUTHOR_ID",
"WORK_TYPE",
"FILE_NAME",
"FILE_LENGTH",
"FILE_SIZE",
"RECORDING_STARTED_DATE",
"RECORDING_FINISHED_DATE",
"UPLOAD_DATE",
"TRANSCRIPTION_STARTED_DATE",
"TRANSCRIPTION_FINISHED_DATE",
] as const;
/**
*
*/
export const SORT_DIRECTIONS = ['ASC', 'DESC'] as const;
export const SORT_DIRECTIONS = ["ASC", "DESC"] as const;
/**
*
@ -198,18 +204,18 @@ export const TAG_MAX_COUNT = 20;
*
*/
export const PNS = {
WNS: 'wns',
APNS: 'apns',
WNS: "wns",
APNS: "apns",
};
/**
*
*/
export const USER_LICENSE_STATUS = {
NORMAL: 'Normal',
NO_LICENSE: 'NoLicense',
ALERT: 'Alert',
RENEW: 'Renew',
NORMAL: "Normal",
NO_LICENSE: "NoLicense",
ALERT: "Alert",
RENEW: "Renew",
};
/**
@ -234,9 +240,9 @@ export const WORKTYPE_MAX_COUNT = 20;
* worktypeのDefault値の取りうる値
**/
export const OPTION_ITEM_VALUE_TYPE = {
DEFAULT: 'Default',
BLANK: 'Blank',
LAST_INPUT: 'LastInput',
DEFAULT: "Default",
BLANK: "Blank",
LAST_INPUT: "LastInput",
} as const;
/**
@ -244,20 +250,20 @@ export const OPTION_ITEM_VALUE_TYPE = {
* @const {string[]}
*/
export const ADB2C_SIGN_IN_TYPE = {
EMAILADDRESS: 'emailAddress',
EMAILADDRESS: "emailAddress",
} as const;
/**
* MANUAL_RECOVERY_REQUIRED
* @const {string}
*/
export const MANUAL_RECOVERY_REQUIRED = '[MANUAL_RECOVERY_REQUIRED]';
export const MANUAL_RECOVERY_REQUIRED = "[MANUAL_RECOVERY_REQUIRED]";
/**
*
* @const {string[]}
*/
export const TERM_TYPE = {
EULA: 'EULA',
DPA: 'DPA',
EULA: "EULA",
DPA: "DPA",
} as const;

View File

@ -8,6 +8,7 @@ import {
UpdateDateColumn,
OneToOne,
JoinColumn,
OneToMany,
} from "typeorm";
@Entity({ name: "accounts" })
@ -73,4 +74,7 @@ export class Account {
@OneToOne(() => User, (user) => user.id)
@JoinColumn({ name: "secondary_admin_user_id" })
secondaryAdminUser: User | null;
@OneToMany(() => User, (user) => user.id)
user: User[] | null;
}

View File

@ -6,6 +6,7 @@ import {
UpdateDateColumn,
JoinColumn,
OneToOne,
ManyToOne,
} from "typeorm";
import { bigintTransformer } from "../common/entity";
import { User } from "./user.entity";
@ -61,3 +62,54 @@ export class License {
@JoinColumn({ name: "allocated_user_id" })
user: User | null;
}
@Entity({ name: "license_allocation_history" })
export class LicenseAllocationHistory {
@PrimaryGeneratedColumn()
id: number;
@Column()
user_id: number;
@Column()
license_id: number;
@Column()
is_allocated: boolean;
@Column()
account_id: number;
@Column()
executed_at: Date;
@Column()
switch_from_type: string;
@Column({ nullable: true, type: "datetime" })
deleted_at: Date | null;
@Column({ nullable: true, type: "datetime" })
created_by: string | null;
@CreateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
created_at: Date;
@Column({ nullable: true, type: "datetime" })
updated_by: string | null;
@UpdateDateColumn({
default: () => "datetime('now', 'localtime')",
type: "datetime",
})
updated_at: Date;
@ManyToOne(() => License, (licenses) => licenses.id, {
createForeignKeyConstraints: false,
}) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定
@JoinColumn({ name: "license_id" })
license: License | null;
}

View File

@ -5,8 +5,11 @@ import {
CreateDateColumn,
UpdateDateColumn,
OneToOne,
JoinColumn,
ManyToOne,
} from "typeorm";
import { License } from "./license.entity";
import { Account } from "./account.entity";
@Entity({ name: "users" })
export class User {
@ -73,6 +76,12 @@ export class User {
}) // defaultはSQLite用設定値.本番用は別途migrationで設定
updated_at: Date;
@ManyToOne(() => Account, (account) => account.user, {
createForeignKeyConstraints: false,
}) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定
@JoinColumn({ name: "account_id" })
account: Account | null;
@OneToOne(() => License, (license) => license.user)
license: License | null;
}

View File

@ -0,0 +1,380 @@
import { app, InvocationContext, Timer } from "@azure/functions";
import { Between, DataSource, In, MoreThan, Repository } from "typeorm";
import { User } from "../entity/user.entity";
import { Account } from "../entity/account.entity";
import { License, LicenseAllocationHistory } from "../entity/license.entity";
import * as dotenv from "dotenv";
import {
LICENSE_ALLOCATED_STATUS,
LICENSE_TYPE,
SWITCH_FROM_TYPE,
TIERS,
USER_ROLES,
} from "../constants";
import {
DateWithDayEndTime,
DateWithNextDayEndTime,
DateWithZeroTime,
NewAllocatedLicenseExpirationDate,
} from "../common/types/types";
export async function licenseAutoAllocationProcessing(
context: InvocationContext,
datasource: DataSource
): Promise<void> {
try {
context.log("[IN]licenseAutoAllocationProcessing");
// ライセンスの有効期間判定用
const currentDateZeroTime = new DateWithZeroTime();
const currentDateEndTime = new DateWithDayEndTime();
// 自動更新対象の候補となるアカウントを取得
const accountRepository = datasource.getRepository(Account);
const targetAccounts = await accountRepository.find({
where: {
tier: TIERS.TIER5,
},
});
// 自動更新対象となるアカウント・ユーザーを取得
const autoAllocationLists = await findTargetUser(
context,
datasource,
targetAccounts,
currentDateZeroTime,
currentDateEndTime
);
// 対象となるアカウント数分ループ
for (const autoAllocationList of autoAllocationLists) {
// ライセンスを割り当てる
await allocateLicense(
context,
datasource,
autoAllocationList,
currentDateZeroTime,
currentDateEndTime
);
}
} catch (e) {
context.log("licenseAutoAllocationProcessing failed.");
context.error(e);
throw e;
} finally {
context.log("[OUT]licenseAutoAllocationProcessing");
}
}
export async function licenseAutoAllocation(
myTimer: Timer,
context: InvocationContext
): Promise<void> {
try {
context.log("[IN]licenseAutoAllocation");
dotenv.config({ path: ".env" });
dotenv.config({ path: ".env.local", override: true });
let datasource: DataSource;
try {
datasource = new DataSource({
type: "mysql",
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: [User, Account, License, LicenseAllocationHistory],
});
await datasource.initialize();
} catch (e) {
context.log("database initialize failed.");
context.error(e);
throw e;
}
await licenseAutoAllocationProcessing(context, datasource);
} catch (e) {
context.log("licenseAutoAllocation failed.");
context.error(e);
throw e;
} finally {
context.log("[OUT]licenseAutoAllocation");
}
}
/**
*
* @param context
* @param datasource
* @param targetAccounts
* @param currentDateZeroTime
* @param currentDateEndTime
* @returns autoAllocationList[] IDリスト
*/
export async function findTargetUser(
context: InvocationContext,
datasource: DataSource,
targetAccounts: Account[],
currentDateZeroTime: DateWithZeroTime,
currentDateEndTime: DateWithDayEndTime
): Promise<autoAllocationList[]> {
try {
context.log("[IN]findTargetUser");
const autoAllocationList = [] as autoAllocationList[];
// ライセンス期限が今日で自動更新対象のユーザーを取得
const userRepository = datasource.getRepository(User);
for (const account of targetAccounts) {
// Author→Typist→Noneの優先度で割り当てたいので、roleごとに個別で取得
const targetAuthorUsers = await userRepository.find({
where: {
account_id: account.id,
auto_renew: true,
role: USER_ROLES.AUTHOR,
license: {
expiry_date: Between(currentDateZeroTime, currentDateEndTime),
},
},
relations: {
license: true,
},
});
const targetTypistUsers = await userRepository.find({
where: {
account_id: account.id,
auto_renew: true,
role: USER_ROLES.TYPIST,
license: {
expiry_date: Between(currentDateZeroTime, currentDateEndTime),
},
},
relations: {
license: true,
},
});
const targetNoneUsers = await userRepository.find({
where: {
account_id: account.id,
auto_renew: true,
role: USER_ROLES.NONE,
license: {
expiry_date: Between(currentDateZeroTime, currentDateEndTime),
},
},
relations: {
license: true,
},
});
// Author→Typist→Noneの順で配列に格納
const userIds = [] as number[];
for (const user of targetAuthorUsers) {
userIds.push(Number(user.id));
}
for (const user of targetTypistUsers) {
userIds.push(Number(user.id));
}
for (const user of targetNoneUsers) {
userIds.push(Number(user.id));
}
// 対象ユーザーが0件なら自動更新リストには含めない
if (userIds.length !== 0) {
autoAllocationList.push({
accountId: account.id,
userIds: userIds,
});
}
}
return autoAllocationList;
} catch (e) {
context.error(e);
context.log("findTargetUser failed.");
throw e;
} finally {
context.log("[OUT]findTargetUser");
}
}
/**
*
* @param context
* @param licenseRepository
* @param accountId ID
* @returns License
*/
export async function getAutoAllocatableLicense(
context: InvocationContext,
licenseRepository: Repository<License>,
accountId: number
): Promise<License | undefined> {
try {
const currentNextDateTime = new DateWithNextDayEndTime();
// 割り当て可能なライセンスを取得
const license = await licenseRepository.findOne({
where: {
account_id: accountId,
status: In([
LICENSE_ALLOCATED_STATUS.REUSABLE,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
]),
expiry_date: MoreThan(currentNextDateTime),
},
order: {
expiry_date: "ASC",
},
});
if (!license) {
// 割り当て可能なライセンスが存在しない場合でもエラーとはしたくないので、undifinedを返却する
return undefined;
}
return license;
} catch (e) {
context.error(e);
context.log("getAutoAllocatableLicense failed.");
throw e;
}
}
/**
*
* @param context
* @param datasource
* @param account ID
* @param currentDateZeroTime
* @param currentDateEndTime
*/
export async function allocateLicense(
context: InvocationContext,
datasource: DataSource,
autoAllocationList: autoAllocationList,
currentDateZeroTime: DateWithZeroTime,
currentDateEndTime: DateWithDayEndTime
): Promise<void> {
try {
context.log("[IN]allocateLicense");
// 自動更新対象ユーザーにライセンスを割り当てる
let hasAllocatebleLicense = true;
for (const userId of autoAllocationList.userIds) {
await datasource.transaction(async (entityManager) => {
const licenseRepository = entityManager.getRepository(License);
const licenseAllocationHistoryRepo = entityManager.getRepository(
LicenseAllocationHistory
);
// 割り当て可能なライセンスを取得する(自動割り当て用)
const autoAllocatableLicense = await getAutoAllocatableLicense(
context,
licenseRepository,
autoAllocationList.accountId
);
// 割り当て可能なライセンスが存在しなければreturnし、その後ループ終了
if (!autoAllocatableLicense) {
context.log(`allocatable license not exist.`);
hasAllocatebleLicense = false;
return;
}
// ライセンスが直前で手動割り当てされていたら、自動割り当てしない
const allocatedLicense = await licenseRepository.findOne({
where: {
allocated_user_id: userId,
expiry_date: Between(currentDateZeroTime, currentDateEndTime),
},
});
if (!allocatedLicense) {
context.log(`skip auto allocation. userID:${userId}`);
return;
}
// 古いライセンスの割り当て解除
allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE;
allocatedLicense.allocated_user_id = null;
await licenseRepository.save(allocatedLicense);
// ライセンス割り当て履歴テーブルへ登録
const deallocationHistory = new LicenseAllocationHistory();
deallocationHistory.user_id = userId;
deallocationHistory.license_id = allocatedLicense.id;
deallocationHistory.account_id = autoAllocationList.accountId;
deallocationHistory.is_allocated = false;
deallocationHistory.executed_at = new Date();
deallocationHistory.switch_from_type = SWITCH_FROM_TYPE.NONE;
await licenseAllocationHistoryRepo.save(deallocationHistory);
// 新規ライセンス割り当て
autoAllocatableLicense.status = LICENSE_ALLOCATED_STATUS.ALLOCATED;
autoAllocatableLicense.allocated_user_id = userId;
// 有効期限が未設定なら365日後に設定
if (!autoAllocatableLicense.expiry_date) {
autoAllocatableLicense.expiry_date =
new NewAllocatedLicenseExpirationDate();
}
await licenseRepository.save(autoAllocatableLicense);
context.log(
`license allocated. userID:${userId}, licenseID:${autoAllocatableLicense.id}`
);
// ライセンス割り当て履歴テーブルを更新するための処理
// 直近割り当てたライセンス種別を取得
const oldLicenseType = await licenseAllocationHistoryRepo.findOne({
relations: {
license: true,
},
where: { user_id: userId, is_allocated: true },
order: { executed_at: "DESC" },
});
let switchFromType = "";
if (oldLicenseType && oldLicenseType.license) {
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;
}
} else {
switchFromType = SWITCH_FROM_TYPE.NONE;
}
// ライセンス割り当て履歴テーブルへ登録
const allocationHistory = new LicenseAllocationHistory();
allocationHistory.user_id = userId;
allocationHistory.license_id = autoAllocatableLicense.id;
allocationHistory.account_id = autoAllocationList.accountId;
allocationHistory.is_allocated = true;
allocationHistory.executed_at = new Date();
// TODO switchFromTypeの値については「PBI1234: 第一階層として、ライセンス数推移情報をCSV出力したい」で正式対応
allocationHistory.switch_from_type = switchFromType;
await licenseAllocationHistoryRepo.save(allocationHistory);
});
// 割り当て可能なライセンスが存在しなければループ終了
if (!hasAllocatebleLicense) {
break;
}
}
} catch (e) {
// エラーが発生しても次のアカウントへの処理は継続させるため、例外をthrowせずにreturnだけする
context.error(e);
context.log("allocateLicense failed.");
return;
} finally {
context.log("[OUT]allocateLicense");
}
}
app.timer("licenseAutoAllocation", {
schedule: "0 0 16 * * *",
handler: licenseAutoAllocation,
});
class autoAllocationList {
accountId: number;
userIds: number[];
}

View File

@ -3,7 +3,7 @@ import { DataSource } from "typeorm";
import { User } from "../../entity/user.entity";
import { Account } from "../../entity/account.entity";
import { ADMIN_ROLES, USER_ROLES } from "../../constants";
import { License } from "../../entity/license.entity";
import { License, LicenseAllocationHistory } from "../../entity/license.entity";
type InitialTestDBState = {
tier1Accounts: { account: Account; users: User[] }[];
@ -196,3 +196,34 @@ export const createLicense = async (
});
identifiers.pop() as License;
};
export const selectLicenseByAllocatedUser = async (
datasource: DataSource,
userId: number
): Promise<{ license: License | null }> => {
const license = await datasource.getRepository(License).findOne({
where: {
allocated_user_id: userId,
},
});
return { license };
};
export const selectLicenseAllocationHistory = async (
datasource: DataSource,
userId: number,
licence_id: number
): Promise<{ licenseAllocationHistory: LicenseAllocationHistory | null }> => {
const licenseAllocationHistory = await datasource
.getRepository(LicenseAllocationHistory)
.findOne({
where: {
user_id: userId,
license_id: licence_id,
},
order: {
executed_at: "DESC",
},
});
return { licenseAllocationHistory };
};

View File

@ -0,0 +1,425 @@
import { DataSource } from "typeorm";
import { licenseAutoAllocationProcessing } from "../functions/licenseAutoAllocation";
import {
ADMIN_ROLES,
LICENSE_ALLOCATED_STATUS,
LICENSE_TYPE,
SWITCH_FROM_TYPE,
TIERS,
USER_ROLES,
} from "../constants";
import {
DateWithDayEndTime,
DateWithNextDayEndTime,
DateWithZeroTime,
ExpirationThresholdDate,
NewAllocatedLicenseExpirationDate,
} from "../common/types/types";
import {
makeTestAccount,
createLicense,
makeTestUser,
selectLicenseByAllocatedUser,
selectLicenseAllocationHistory,
} from "./common/utility";
import * as dotenv from "dotenv";
import { InvocationContext } from "@azure/functions";
describe("licenseAlert", () => {
dotenv.config({ path: ".env" });
dotenv.config({ path: ".env.local", override: true });
let source: DataSource | null = 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 () => {
if (!source) return;
await source.destroy();
source = null;
});
it("有効期限が本日のライセンスが自動更新されること", async () => {
if (!source) fail();
const context = new InvocationContext();
const currentDateEndTime = new DateWithDayEndTime();
console.log(currentDateEndTime);
// アカウント
const account1 = await makeTestAccount(
source,
{ tier: 5 },
{ role: `${USER_ROLES.NONE}` }
);
const account2 = await makeTestAccount(
source,
{ tier: 5 },
{ role: `${USER_ROLES.NONE}` }
);
// 更新対象のユーザー3role分
const user1 = await makeTestUser(source, {
account_id: account1.account.id,
role: `${USER_ROLES.NONE}`,
});
const user2 = await makeTestUser(source, {
account_id: account1.account.id,
role: `${USER_ROLES.AUTHOR}`,
});
const user3 = await makeTestUser(source, {
account_id: account1.account.id,
role: `${USER_ROLES.TYPIST}`,
});
// 更新対象ではないユーザー(まだ有効期限が残っている)
const user4 = await makeTestUser(source, {
account_id: account1.account.id,
role: `${USER_ROLES.NONE}`,
});
// 更新対象ではないユーザーauto_renewがfalse
const user5 = await makeTestUser(source, {
account_id: account1.account.id,
role: `${USER_ROLES.NONE}`,
auto_renew: false,
});
// 割り当て済みで有効期限が本日のライセンス
await createLicense(
source,
1,
currentDateEndTime,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
user1.id,
null,
null,
null
);
await createLicense(
source,
2,
currentDateEndTime,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
user2.id,
null,
null,
null
);
await createLicense(
source,
3,
currentDateEndTime,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
user3.id,
null,
null,
null
);
await createLicense(
source,
20,
currentDateEndTime,
account2.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
account2.admin.id,
null,
null,
null
);
await createLicense(
source,
5,
currentDateEndTime,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
user5.id,
null,
null,
null
);
// 割り当て済みの更新対象ではないライセンス
const nextDate = new Date();
nextDate.setDate(nextDate.getDate() + 1);
nextDate.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定
nextDate.setMilliseconds(0);
await createLicense(
source,
4,
nextDate,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
user4.id,
null,
null,
null
);
// 有効期限が先の未割当ライセンスを作成
// idが100101のものは有効期限が当日、翌日なので自動割り当て対象外
// idが102のものから割り当てられる
for (let i = 0; i < 10; i++) {
const date = new Date();
date.setDate(date.getDate() + i);
date.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定
date.setMilliseconds(0);
await createLicense(
source,
i + 100,
date,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null
);
}
const date = new Date();
date.setDate(date.getDate() + 30);
date.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定
date.setMilliseconds(0);
await createLicense(
source,
200,
date,
account2.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.REUSABLE,
null,
null,
null,
null
);
await licenseAutoAllocationProcessing(context, source);
const user1Allocated = await selectLicenseByAllocatedUser(source, user1.id);
const user2Allocated = await selectLicenseByAllocatedUser(source, user2.id);
const user3Allocated = await selectLicenseByAllocatedUser(source, user3.id);
const user4Allocated = await selectLicenseByAllocatedUser(source, user4.id);
const user5Allocated = await selectLicenseByAllocatedUser(source, user5.id);
const admin2Allocated = await selectLicenseByAllocatedUser(
source,
account2.admin.id
);
const licenseAllocationHistory = await selectLicenseAllocationHistory(
source,
user1.id,
104
);
// Author、Typist、Noneの優先順位で割り当てられていることを確認
expect(user1Allocated.license?.id).toBe(104);
expect(user2Allocated.license?.id).toBe(102);
expect(user3Allocated.license?.id).toBe(103);
// 有効期限がまだあるので、ライセンスが更新されていないことを確認
expect(user4Allocated.license?.id).toBe(4);
// auto_renewがfalseなので、ライセンスが更新されていないことを確認
expect(user5Allocated.license?.id).toBe(5);
// 複数アカウント分の処理が正常に行われていることの確認
expect(admin2Allocated.license?.id).toBe(200);
// ライセンス割り当て履歴テーブルが更新されていることを確認
expect(licenseAllocationHistory.licenseAllocationHistory?.user_id).toBe(
user1.id
);
expect(
licenseAllocationHistory.licenseAllocationHistory?.is_allocated
).toBe(true);
expect(licenseAllocationHistory.licenseAllocationHistory?.account_id).toBe(
account1.account.id
);
});
it("新たに割り当てられるライセンスが存在しないため、ライセンスが自動更新されない(エラーではない)", async () => {
if (!source) fail();
const context = new InvocationContext();
const currentDateEndTime = new DateWithDayEndTime();
console.log(currentDateEndTime);
// アカウント
const account1 = await makeTestAccount(
source,
{ tier: 5 },
{ role: `${USER_ROLES.NONE}` }
);
// 更新対象のユーザー3role分
const user1 = await makeTestUser(source, {
account_id: account1.account.id,
role: `${USER_ROLES.NONE}`,
});
const user2 = await makeTestUser(source, {
account_id: account1.account.id,
role: `${USER_ROLES.AUTHOR}`,
});
const user3 = await makeTestUser(source, {
account_id: account1.account.id,
role: `${USER_ROLES.TYPIST}`,
});
// 割り当て済みで有効期限が本日のライセンス
await createLicense(
source,
1,
currentDateEndTime,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
user1.id,
null,
null,
null
);
await createLicense(
source,
2,
currentDateEndTime,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
user2.id,
null,
null,
null
);
await createLicense(
source,
3,
currentDateEndTime,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
user3.id,
null,
null,
null
);
await licenseAutoAllocationProcessing(context, source);
const user1Allocated = await selectLicenseByAllocatedUser(source, user1.id);
const user2Allocated = await selectLicenseByAllocatedUser(source, user2.id);
const user3Allocated = await selectLicenseByAllocatedUser(source, user3.id);
// ライセンスが更新されていないことを確認
expect(user1Allocated.license?.id).toBe(1);
expect(user2Allocated.license?.id).toBe(2);
expect(user3Allocated.license?.id).toBe(3);
});
it("tier4のアカウントのため、ライセンスが自動更新されない", async () => {
if (!source) fail();
const context = new InvocationContext();
const currentDateEndTime = new DateWithDayEndTime();
console.log(currentDateEndTime);
// アカウント
const account1 = await makeTestAccount(
source,
{ tier: 4 },
{ role: `${USER_ROLES.NONE}` }
);
// 更新対象のユーザー3role分
const user1 = await makeTestUser(source, {
account_id: account1.account.id,
role: `${USER_ROLES.NONE}`,
});
const user2 = await makeTestUser(source, {
account_id: account1.account.id,
role: `${USER_ROLES.AUTHOR}`,
});
const user3 = await makeTestUser(source, {
account_id: account1.account.id,
role: `${USER_ROLES.TYPIST}`,
});
// 割り当て済みで有効期限が本日のライセンス
await createLicense(
source,
1,
currentDateEndTime,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
user1.id,
null,
null,
null
);
await createLicense(
source,
2,
currentDateEndTime,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
user2.id,
null,
null,
null
);
await createLicense(
source,
3,
currentDateEndTime,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.ALLOCATED,
user3.id,
null,
null,
null
);
// 有効期限が先の未割当ライセンスを作成
// idが100101のものは有効期限が当日、翌日なので自動割り当て対象外
// idが102のものから割り当てられる
for (let i = 0; i < 10; i++) {
const date = new Date();
date.setDate(date.getDate() + i);
date.setHours(23, 59, 59); // 時分秒を"23:59:59"に固定
date.setMilliseconds(0);
await createLicense(
source,
i + 100,
date,
account1.account.id,
LICENSE_TYPE.CARD,
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
null,
null,
null,
null
);
}
await licenseAutoAllocationProcessing(context, source);
const user1Allocated = await selectLicenseByAllocatedUser(source, user1.id);
const user2Allocated = await selectLicenseByAllocatedUser(source, user2.id);
const user3Allocated = await selectLicenseByAllocatedUser(source, user3.id);
// ライセンスが更新されていないことを確認
expect(user1Allocated.license?.id).toBe(1);
expect(user2Allocated.license?.id).toBe(2);
expect(user3Allocated.license?.id).toBe(3);
});
});