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, DateWithZeroTime, NewAllocatedLicenseExpirationDate, } from "../common/types/types"; export async function licenseAutoAllocationProcessing( context: InvocationContext, datasource: DataSource, dateToTrigger?: Date ): Promise { try { context.log("[IN]licenseAutoAllocationProcessing"); // ライセンスの有効期間判定用 let currentDateZeroTime = new DateWithZeroTime(); let currentDateEndTime = new DateWithDayEndTime(); if (dateToTrigger) { currentDateZeroTime = new DateWithZeroTime(dateToTrigger); currentDateEndTime = new DateWithDayEndTime(dateToTrigger); } // 自動更新対象の候補となるアカウントを取得 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 { 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 { 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, accountId: number, currentDateEndTime: DateWithDayEndTime ): Promise { try { context.log("[IN]getAutoAllocatableLicense"); // 割り当て可能なライセンスを取得 const license = await licenseRepository.findOne({ where: { account_id: accountId, status: In([ LICENSE_ALLOCATED_STATUS.REUSABLE, LICENSE_ALLOCATED_STATUS.UNALLOCATED, ]), expiry_date: MoreThan(currentDateEndTime) || null, }, order: { expiry_date: "ASC", }, }); if (!license) { // 割り当て可能なライセンスが存在しない場合でもエラーとはしたくないので、undifinedを返却する return undefined; } return license; } catch (e) { context.error(e); context.log("getAutoAllocatableLicense failed."); throw e; } finally { context.log("[OUT]getAutoAllocatableLicense"); } } /** * ライセンスを割り当てる * @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 { 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, currentDateEndTime ); // 割り当て可能なライセンスが存在しなければ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[]; }