## 概要 [Task3531: パイプラインエラー対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3531) - パイプラインエラー解消 ## レビューポイント - 共有
639 lines
21 KiB
TypeScript
639 lines
21 KiB
TypeScript
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";
|
||
import { createRedisClient } from "../redis/redis";
|
||
import { RedisClient } from "redis";
|
||
import { promisify } from "util";
|
||
import { makeSendCompKey } from "../common/cache";
|
||
import {
|
||
MAIL_U103,
|
||
MAIL_U104,
|
||
SEND_COMPLETE_PREFIX,
|
||
DONE,
|
||
} from "../common/cache/constants";
|
||
|
||
export async function licenseAlertProcessing(
|
||
context: InvocationContext,
|
||
datasource: DataSource,
|
||
redisClient: RedisClient,
|
||
sendgrid: SendGridService,
|
||
adb2c: AdB2cService
|
||
) {
|
||
try {
|
||
context.log("[IN]licenseAlertProcessing");
|
||
|
||
// redisのキー用
|
||
const currentDate = new DateWithZeroTime();
|
||
const formattedDate = `${currentDate.getFullYear()}-${(
|
||
currentDate.getMonth() + 1
|
||
).toString()}-${currentDate.getDate().toString()}`;
|
||
const keysAsync = promisify(redisClient.keys).bind(redisClient);
|
||
|
||
// メール送信対象のアカウント情報を取得
|
||
const sendTargetAccounts = await getAlertMailTargetAccount(
|
||
context,
|
||
datasource
|
||
);
|
||
|
||
// adb2cからメールアドレスを取得し、上記で取得したアカウントにマージする
|
||
const sendTargetAccountsMargedAdb2c = await createAccountInfo(
|
||
context,
|
||
redisClient,
|
||
adb2c,
|
||
sendTargetAccounts
|
||
);
|
||
|
||
// メール送信
|
||
await sendAlertMail(
|
||
context,
|
||
redisClient,
|
||
sendgrid,
|
||
sendTargetAccountsMargedAdb2c,
|
||
formattedDate
|
||
);
|
||
|
||
// 最後まで処理が正常に通ったら、redisに保存した送信情報を削除する
|
||
try {
|
||
const delAsync = promisify(redisClient.del).bind(redisClient);
|
||
|
||
const keys = await keysAsync(`${SEND_COMPLETE_PREFIX}${formattedDate}*`);
|
||
console.log(`delete terget:${keys}`);
|
||
if (keys.length > 0) {
|
||
const delResult = await delAsync(keys);
|
||
console.log(`delete number:${delResult}`);
|
||
}
|
||
} catch (e) {
|
||
context.log("redis delete failed");
|
||
throw e;
|
||
}
|
||
} catch (e) {
|
||
throw e;
|
||
} finally {
|
||
context.log("[OUT]licenseAlertProcessing");
|
||
}
|
||
}
|
||
|
||
export async function licenseAlert(
|
||
myTimer: Timer,
|
||
context: InvocationContext
|
||
): Promise<void> {
|
||
context.log("[IN]licenseAlert");
|
||
|
||
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],
|
||
});
|
||
await datasource.initialize();
|
||
} catch (e) {
|
||
context.log("database initialize failed.");
|
||
context.error(e);
|
||
throw e;
|
||
}
|
||
|
||
let redisClient: RedisClient;
|
||
try {
|
||
// redis接続
|
||
redisClient = createRedisClient();
|
||
} catch (e) {
|
||
context.log("redis client create failed.");
|
||
context.error(e);
|
||
throw e;
|
||
}
|
||
|
||
try {
|
||
const adb2c = new AdB2cService();
|
||
const sendgrid = new SendGridService();
|
||
await licenseAlertProcessing(
|
||
context,
|
||
datasource,
|
||
redisClient,
|
||
sendgrid,
|
||
adb2c
|
||
);
|
||
} catch (e) {
|
||
context.log("licenseAlertProcessing failed.");
|
||
context.error(e);
|
||
throw e;
|
||
} finally {
|
||
await datasource.destroy();
|
||
redisClient.quit;
|
||
context.log("[OUT]licenseAlert");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* アラートメールを送信する対象のアカウントを取得する
|
||
* @param context
|
||
* @param datasource
|
||
* @returns accountInfo[] メール送信対象のアカウント情報
|
||
*/
|
||
async function getAlertMailTargetAccount(
|
||
context: InvocationContext,
|
||
datasource: DataSource
|
||
): Promise<accountInfo[]> {
|
||
try {
|
||
context.log("[IN]getAlertMailTargetAccount");
|
||
const currentDate = new DateWithZeroTime();
|
||
const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime());
|
||
const currentDateWithZeroTime = new DateWithZeroTime();
|
||
const currentDateWithDayEndTime = new DateWithDayEndTime();
|
||
|
||
// 第五のアカウントを取得
|
||
const accountRepository = datasource.getRepository(Account);
|
||
const accounts = await accountRepository.find({
|
||
where: {
|
||
tier: TIERS.TIER5,
|
||
},
|
||
relations: {
|
||
primaryAdminUser: true,
|
||
secondaryAdminUser: true,
|
||
},
|
||
});
|
||
|
||
const sendTargetAccounts = [] as accountInfo[];
|
||
const licenseRepository = datasource.getRepository(License);
|
||
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,
|
||
});
|
||
}
|
||
return sendTargetAccounts;
|
||
} catch (e) {
|
||
context.error(e);
|
||
context.log("getAlertMailTargetAccount failed.");
|
||
throw e;
|
||
} finally {
|
||
context.log("[OUT]getAlertMailTargetAccount");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Azure AD B2Cからユーザ情報を取得し、アカウント情報を作成する
|
||
* @param context
|
||
* @param redisClient
|
||
* @param adb2c
|
||
* @param sendTargetAccounts RDBから取得したアカウント情報
|
||
* @returns accountInfo[] メール送信対象のアカウント情報
|
||
*/
|
||
async function createAccountInfo(
|
||
context: InvocationContext,
|
||
redisClient: RedisClient,
|
||
adb2c: AdB2cService,
|
||
sendTargetAccounts: accountInfo[]
|
||
): Promise<accountInfo[]> {
|
||
// ADB2Cからユーザーを取得する用の外部ID配列を作成
|
||
const externalIds = [] as string[];
|
||
sendTargetAccounts.forEach((x) => {
|
||
if (x.primaryAdminExternalId) {
|
||
externalIds.push(x.primaryAdminExternalId);
|
||
}
|
||
if (x.secondaryAdminExternalId) {
|
||
externalIds.push(x.secondaryAdminExternalId);
|
||
}
|
||
});
|
||
const adb2cUsers = await adb2c.getUsers(context, redisClient, externalIds);
|
||
if (adb2cUsers.length === 0) {
|
||
context.log("Target user not found");
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
return sendTargetAccounts;
|
||
}
|
||
|
||
/**
|
||
* アラートメールを送信する
|
||
* @param context
|
||
* @param redisClient
|
||
* @param sendgrid
|
||
* @param sendTargetAccounts メール送信対象のアカウント情報
|
||
* @param formattedDate redisのキーに使用する日付
|
||
* @returns ユーザ情報
|
||
*/
|
||
async function sendAlertMail(
|
||
context: InvocationContext,
|
||
redisClient: RedisClient,
|
||
sendgrid: SendGridService,
|
||
sendTargetAccounts: accountInfo[],
|
||
formattedDate: string
|
||
): Promise<void> {
|
||
try {
|
||
context.log("[IN]sendAlertMail");
|
||
|
||
// redis用
|
||
const getAsync = promisify(redisClient.get).bind(redisClient);
|
||
const setexAsync = promisify(redisClient.setex).bind(redisClient);
|
||
const ttl = process.env.ADB2C_CACHE_TTL;
|
||
const mailFrom = getMailFrom();
|
||
|
||
for (const targetAccount of sendTargetAccounts) {
|
||
// プライマリ管理者が入っているかチェック
|
||
// 入っていない場合は、アラートメールを送信する必要が無いため、何も処理をせず次のループへ
|
||
if (targetAccount.primaryAdminExternalId) {
|
||
// メール送信
|
||
// strictNullChecks対応
|
||
if (!targetAccount.primaryAdminEmail) {
|
||
continue;
|
||
}
|
||
// ライセンス不足メール
|
||
if (targetAccount.shortage !== 0) {
|
||
// redisに送信履歴がない場合のみ送信する
|
||
const mailResult = await getAsync(
|
||
makeSendCompKey(
|
||
formattedDate,
|
||
targetAccount.primaryAdminExternalId,
|
||
MAIL_U103
|
||
)
|
||
);
|
||
if (mailResult !== DONE) {
|
||
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}`
|
||
);
|
||
// 送信成功時、成功履歴をredisに保存
|
||
try {
|
||
const key = makeSendCompKey(
|
||
formattedDate,
|
||
targetAccount.primaryAdminExternalId,
|
||
MAIL_U103
|
||
);
|
||
await setexAsync(key, ttl, DONE);
|
||
context.log(
|
||
"setex Result:",
|
||
`key:${key},ttl:${ttl},value:Done`
|
||
);
|
||
} catch (e) {
|
||
context.error(e);
|
||
context.log(
|
||
"setex failed.",
|
||
`target: ${targetAccount.primaryAdminEmail}`
|
||
);
|
||
}
|
||
} catch (e) {
|
||
context.error(e);
|
||
context.log(
|
||
`Shortage mail send failed. mail to :${targetAccount.primaryAdminEmail}`
|
||
);
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
// セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
|
||
if (
|
||
targetAccount.secondaryAdminEmail &&
|
||
targetAccount.secondaryAdminExternalId
|
||
) {
|
||
// redisに送信履歴がない場合のみ送信する
|
||
const mailResult = await getAsync(
|
||
makeSendCompKey(
|
||
formattedDate,
|
||
targetAccount.secondaryAdminExternalId,
|
||
MAIL_U103
|
||
)
|
||
);
|
||
if (mailResult !== DONE) {
|
||
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}`
|
||
);
|
||
// 送信成功時、成功履歴をredisに保存
|
||
try {
|
||
const key = makeSendCompKey(
|
||
formattedDate,
|
||
targetAccount.secondaryAdminExternalId,
|
||
MAIL_U103
|
||
);
|
||
await setexAsync(key, ttl, DONE);
|
||
context.log(
|
||
"setex Result:",
|
||
`key:${key},ttl:${ttl},value:Done`
|
||
);
|
||
} catch (e) {
|
||
context.error(e);
|
||
context.log(
|
||
"setex failed.",
|
||
`target: ${targetAccount.secondaryAdminEmail}`
|
||
);
|
||
}
|
||
} catch (e) {
|
||
context.error(e);
|
||
context.log(
|
||
`Shortage mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
|
||
);
|
||
throw e;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ライセンス失効警告メール
|
||
if (targetAccount.userCountOfLicenseExpiringSoon !== 0) {
|
||
// redisに送信履歴がない場合のみ送信する
|
||
const mailResult = await getAsync(
|
||
makeSendCompKey(
|
||
formattedDate,
|
||
targetAccount.primaryAdminExternalId,
|
||
MAIL_U104
|
||
)
|
||
);
|
||
if (mailResult !== DONE) {
|
||
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}`
|
||
);
|
||
// 送信成功時、成功履歴をredisに保存
|
||
try {
|
||
const key = makeSendCompKey(
|
||
formattedDate,
|
||
targetAccount.primaryAdminExternalId,
|
||
MAIL_U104
|
||
);
|
||
await setexAsync(key, ttl, DONE);
|
||
context.log(
|
||
"setex Result:",
|
||
`key:${key},ttl:${ttl},value:Done`
|
||
);
|
||
} catch (e) {
|
||
context.error(e);
|
||
context.log(
|
||
"setex failed.",
|
||
`target: ${targetAccount.primaryAdminEmail}`
|
||
);
|
||
}
|
||
} catch (e) {
|
||
context.error(e);
|
||
context.log(
|
||
`Expiring soon mail send failed. mail to :${targetAccount.primaryAdminEmail}`
|
||
);
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
// セカンダリ管理者が存在する場合、セカンダリ管理者にも送信
|
||
if (
|
||
targetAccount.secondaryAdminEmail &&
|
||
targetAccount.secondaryAdminExternalId
|
||
) {
|
||
// redisに送信履歴がない場合のみ送信する
|
||
const mailResult = makeSendCompKey(
|
||
formattedDate,
|
||
targetAccount.secondaryAdminExternalId,
|
||
MAIL_U104
|
||
);
|
||
if (mailResult !== DONE) {
|
||
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}`
|
||
);
|
||
try {
|
||
const key = makeSendCompKey(
|
||
formattedDate,
|
||
targetAccount.secondaryAdminExternalId,
|
||
MAIL_U104
|
||
);
|
||
await setexAsync(key, ttl, DONE);
|
||
context.log(
|
||
"setex Result:",
|
||
`key:${key},ttl:${ttl},value:Done`
|
||
);
|
||
} catch (e) {
|
||
context.error(e);
|
||
context.log(
|
||
"setex failed.",
|
||
`target: ${targetAccount.secondaryAdminEmail}`
|
||
);
|
||
}
|
||
} catch (e) {
|
||
context.error(e);
|
||
context.log(
|
||
`Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}`
|
||
);
|
||
throw e;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
context.log("sendAlertMail failed.");
|
||
throw e;
|
||
} finally {
|
||
context.log("[OUT]sendAlertMail");
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|