import { app, InvocationContext, Timer } from "@azure/functions"; import { Between, DataSource, In, IsNull, MoreThan, Not } from "typeorm"; import { User } from "../entity/user.entity"; import { Account } from "../entity/account.entity"; import { ADB2C_SIGN_IN_TYPE, LICENSE_ALLOCATED_STATUS, TIERS, } from "../constants"; import * as dotenv from "dotenv"; import { License } from "../entity/license.entity"; import { DateWithDayEndTime, DateWithZeroTime, ExpirationThresholdDate, } from "../common/types/types"; import { createMailContentOfLicenseShortage } from "../sendgrid/mailContents/U103ShortageAlert"; import { createMailContentOfLicenseExpiringSoon } from "../sendgrid/mailContents/U104ExpiringSoonAlert"; import { AdB2cService } from "../adb2c/adb2c"; import { SendGridService } from "../sendgrid/sendgrid"; import { getMailFrom } from "../common/getEnv/getEnv"; export async function licenseAlertProcessing( context: InvocationContext, datasource: DataSource, sendgrid: SendGridService, adb2c: AdB2cService ) { context.log("[IN]licenseAlertProcessing"); const mailFrom = getMailFrom(); const accountRepository = datasource.getRepository(Account); // 第五のアカウントを取得 const accounts = await accountRepository.find({ where: { tier: TIERS.TIER5, }, relations: { primaryAdminUser: true, secondaryAdminUser: true, }, }); const licenseRepository = datasource.getRepository(License); const currentDate = new DateWithZeroTime(); const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); const currentDateWithZeroTime = new DateWithZeroTime(); const currentDateWithDayEndTime = new DateWithDayEndTime(); const sendTargetAccounts = [] as accountInfo[]; const counts = async () => { for (const account of accounts) { // 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う const allocatableLicenseWithMargin = await licenseRepository.count({ where: [ { account_id: account.id, status: In([ LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.REUSABLE, ]), expiry_date: MoreThan(expiringSoonDate), }, { account_id: account.id, status: In([ LICENSE_ALLOCATED_STATUS.UNALLOCATED, LICENSE_ALLOCATED_STATUS.REUSABLE, ]), expiry_date: IsNull(), }, ], }); // 有効期限が現在日付からしきい値以内のライセンス数を取得する const expiringSoonLicense = await licenseRepository.count({ where: { account_id: account.id, expiry_date: Between(currentDate, expiringSoonDate), status: Not(LICENSE_ALLOCATED_STATUS.DELETED), }, }); // shortage算出 let shortage = allocatableLicenseWithMargin - expiringSoonLicense; shortage = shortage >= 0 ? 0 : Math.abs(shortage); // AutoRenewが未チェックかつ、有効期限当日のライセンスが割り当てられているユーザー数を取得 const userCount = await licenseRepository.count({ where: [ { account_id: account.id, expiry_date: Between( currentDateWithZeroTime, currentDateWithDayEndTime ), status: LICENSE_ALLOCATED_STATUS.ALLOCATED, user: { auto_renew: false, }, }, ], relations: { user: true, }, }); // 上で取得したshortageとユーザー数のどちらかが1以上ならプライマリ、セカンダリ管理者、親企業名を保持する // (shortageとユーザー数のどちらかが1以上 = アラートメールを送る必要がある) let primaryAdminExternalId: string | undefined; let secondaryAdminExternalId: string | undefined; let parentCompanyName: string | undefined; if (shortage !== 0 || userCount !== 0) { primaryAdminExternalId = account.primaryAdminUser ? account.primaryAdminUser.external_id : undefined; secondaryAdminExternalId = account.secondaryAdminUser ? account.secondaryAdminUser.external_id : undefined; // 第五のアカウントを取得 // strictNullChecks対応 if (account.parent_account_id) { const parent = await accountRepository.findOne({ where: { id: account.parent_account_id, }, }); parentCompanyName = parent?.company_name; } } else { primaryAdminExternalId = undefined; secondaryAdminExternalId = undefined; parentCompanyName = undefined; } sendTargetAccounts.push({ accountId: account.id, companyName: account.company_name, parentCompanyName: parentCompanyName, shortage: shortage, userCountOfLicenseExpiringSoon: userCount, primaryAdminExternalId: primaryAdminExternalId, secondaryAdminExternalId: secondaryAdminExternalId, primaryAdminEmail: undefined, secondaryAdminEmail: undefined, }); } }; await counts(); // ADB2Cからユーザーを取得する用の外部ID配列を作成 const externalIds = [] as string[]; sendTargetAccounts.map((x) => { if (x.primaryAdminExternalId) { externalIds.push(x.primaryAdminExternalId); } if (x.secondaryAdminExternalId) { externalIds.push(x.secondaryAdminExternalId); } }); const adb2cUsers = await adb2c.getUsers(context, externalIds); if (!adb2cUsers) { context.log("Target user not found"); context.log("[OUT]licenseAlertProcessing"); return; } // ADB2Cから取得したメールアドレスをRDBから取得した情報にマージ sendTargetAccounts.map((info) => { const primaryAdminUser = adb2cUsers.find( (adb2c) => info.primaryAdminExternalId === adb2c.id ); if (primaryAdminUser) { const primaryAdminMail = primaryAdminUser.identities?.find( (identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS )?.issuerAssignedId; if (primaryAdminMail) { info.primaryAdminEmail = primaryAdminMail; } const secondaryAdminUser = adb2cUsers.find( (adb2c) => info.secondaryAdminExternalId === adb2c.id ); if (secondaryAdminUser) { const secondaryAdminMail = secondaryAdminUser.identities?.find( (identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS )?.issuerAssignedId; if (secondaryAdminMail) { info.secondaryAdminEmail = secondaryAdminMail; } } } }); const sendMail = async () => { for (const targetAccount of sendTargetAccounts) { // プライマリ管理者が入っているかチェック // 入っていない場合は、アラートメールを送信する必要が無いため、何も処理をせず次のループへ if (targetAccount.primaryAdminExternalId) { // メール送信 // strictNullChecks対応 if (targetAccount.primaryAdminEmail) { // ライセンス不足メール if (targetAccount.shortage !== 0) { const { subject, text, html } = await createMailContentOfLicenseShortage( targetAccount.companyName, targetAccount.shortage, targetAccount.parentCompanyName ); // メールを送信 try { await sendgrid.sendMail( targetAccount.primaryAdminEmail, mailFrom, subject, text, html ); context.log( `Shortage mail send success. mail to :${targetAccount.primaryAdminEmail}` ); } catch { context.log( `Shortage mail send failed. mail to :${targetAccount.primaryAdminEmail}` ); } // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信 if (targetAccount.secondaryAdminEmail) { // ライセンス不足メール if (targetAccount.shortage !== 0) { const { subject, text, html } = await createMailContentOfLicenseShortage( targetAccount.companyName, targetAccount.shortage, targetAccount.parentCompanyName ); // メールを送信 try { await sendgrid.sendMail( targetAccount.secondaryAdminEmail, mailFrom, subject, text, html ); context.log( `Shortage mail send success. mail to :${targetAccount.secondaryAdminEmail}` ); } catch { context.log( `Shortage mail send failed. mail to :${targetAccount.secondaryAdminEmail}` ); } } } } // ライセンス失効警告メール if (targetAccount.userCountOfLicenseExpiringSoon !== 0) { const { subject, text, html } = await createMailContentOfLicenseExpiringSoon( targetAccount.companyName, targetAccount.userCountOfLicenseExpiringSoon, targetAccount.parentCompanyName ); // メールを送信 try { await sendgrid.sendMail( targetAccount.primaryAdminEmail, mailFrom, subject, text, html ); context.log( `Expiring soon mail send success. mail to :${targetAccount.primaryAdminEmail}` ); } catch { context.log( `Expiring soon mail send failed. mail to :${targetAccount.primaryAdminEmail}` ); } // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信 if (targetAccount.secondaryAdminEmail) { // ライセンス不足メール if (targetAccount.shortage !== 0) { const { subject, text, html } = await createMailContentOfLicenseExpiringSoon( targetAccount.companyName, targetAccount.userCountOfLicenseExpiringSoon, targetAccount.parentCompanyName ); // メールを送信 try { await sendgrid.sendMail( targetAccount.secondaryAdminEmail, mailFrom, subject, text, html ); context.log( `Expiring soon mail send success. mail to :${targetAccount.secondaryAdminEmail}` ); } catch { context.log( `Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}` ); } } } } } } } }; await sendMail(); context.log("[OUT]licenseAlertProcessing"); } export async function licenseAlert( myTimer: Timer, context: InvocationContext ): Promise { context.log("[IN]licenseAlert"); dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); const 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], }); await datasource.initialize(); const adb2c = new AdB2cService(); const sendgrid = new SendGridService(); try { await licenseAlertProcessing(context, datasource, sendgrid, adb2c); } catch (e) { context.log("licenseAlertProcessing failed"); context.error(e); } finally { await datasource.destroy(); context.log("[OUT]licenseAlert"); } } app.timer("licenseAlert", { schedule: "0 0 1 * * *", handler: licenseAlert, }); class accountInfo { accountId: number; companyName: string; parentCompanyName: string | undefined; shortage: number; userCountOfLicenseExpiringSoon: number; primaryAdminExternalId: string | undefined; secondaryAdminExternalId: string | undefined; primaryAdminEmail: string | undefined; secondaryAdminEmail: string | undefined; }