Merged PR 641: ライセンス割当完了通知 [U-108] の実装

## 概要
[Task3305: ライセンス割当完了通知 [U-108] の実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3305)

- ライセンス割り当ての完了通知メール送信機能を追加しました。
- テストでメール送信しないようSendGridのメソッドを上書きする処理を追加しました。

## レビューポイント
- テンプレートの適用内容に不自然な点はないでしょうか?
- テストでのメソッドの上書きは適切でしょうか?

## UIの変更
- なし
## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-12-19 09:57:07 +00:00
parent 48ff009d39
commit 9e1bc8944f
8 changed files with 461 additions and 4 deletions

View File

@ -97,6 +97,41 @@ export const overrideSendgridService = <TService>(
customerMail: string,
customerAccountName: string,
) => Promise<void>;
sendMailWithU105?: (
context: Context,
customerMails: string[],
customerAccountName: string,
lisenceCount: number,
poNumber: string,
dealerEmails: string[],
dealerAccountName: string,
) => Promise<void>;
sendMailWithU106?: (
context: Context,
customerMails: string[],
customerAccountName: string,
lisenceCount: number,
poNumber: string,
dealerEmails: string[],
dealerAccountName: string,
) => Promise<void>;
sendMailWithU107?: (
context: Context,
customerMails: string[],
customerAccountName: string,
lisenceCount: number,
poNumber: string,
dealerEmails: string[],
dealerAccountName: string,
) => Promise<void>;
sendMailWithU108?: (
context: Context,
userName: string,
userMail: string,
customerAdminMails: string[],
customerAccountName: string,
dealerAccountName: string,
) => Promise<void>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
@ -131,6 +166,58 @@ export const overrideSendgridService = <TService>(
writable: true,
});
}
if (overrides.sendMailWithU105) {
Object.defineProperty(obj, obj.sendMailWithU105.name, {
value: overrides.sendMailWithU105,
writable: true,
});
} else {
Object.defineProperty(obj, obj.sendMailWithU105.name, {
value: async () => {
return;
},
writable: true,
});
}
if (overrides.sendMailWithU106) {
Object.defineProperty(obj, obj.sendMailWithU106.name, {
value: overrides.sendMailWithU106,
writable: true,
});
} else {
Object.defineProperty(obj, obj.sendMailWithU106.name, {
value: async () => {
return;
},
writable: true,
});
}
if (overrides.sendMailWithU107) {
Object.defineProperty(obj, obj.sendMailWithU107.name, {
value: overrides.sendMailWithU107,
writable: true,
});
} else {
Object.defineProperty(obj, obj.sendMailWithU107.name, {
value: async () => {
return;
},
writable: true,
});
}
if (overrides.sendMailWithU108) {
Object.defineProperty(obj, obj.sendMailWithU108.name, {
value: overrides.sendMailWithU108,
writable: true,
});
} else {
Object.defineProperty(obj, obj.sendMailWithU108.name, {
value: async () => {
return;
},
writable: true,
});
}
if (overrides.createMailContentFromEmailConfirm) {
Object.defineProperty(obj, obj.createMailContentFromEmailConfirm.name, {

View File

@ -2273,6 +2273,12 @@ describe('issueLicense', () => {
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AccountsService>(AccountsService);
overrideSendgridService(service, {
sendMailWithU107: async () => {
return;
},
});
const now = new Date();
// 親と子アカウントを作成する
const { id: parentAccountId } = (
@ -2368,6 +2374,11 @@ describe('issueLicense', () => {
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AccountsService>(AccountsService);
overrideSendgridService(service, {
sendMailWithU107: async () => {
return;
},
});
const now = new Date();
// 親と子アカウントを作成する
const { id: parentAccountId } = (
@ -6125,6 +6136,11 @@ describe('deleteAccountAndData', () => {
const module = await makeTestingModule(source);
if (!module) fail();
const service = module.get<AccountsService>(AccountsService);
overrideSendgridService(service, {
sendMailWithU107: async () => {
return;
},
});
// 第一~第四階層のアカウント作成
const {
tier1Accounts: tier1Accounts,
@ -6232,6 +6248,11 @@ describe('deleteAccountAndData', () => {
const licensesB = await getLicenses(source, tier5AccountsB.account.id);
const usersService = module.get<UsersService>(UsersService);
overrideSendgridService(usersService, {
sendMailWithU108: async () => {
return;
},
});
// アカウントAのライセンスを割り当てる
await usersService.allocateLicense(
context,

View File

@ -41,6 +41,7 @@ import {
makeTestSimpleAccount,
makeTestUser,
} from '../../common/test/utility';
import { overrideSendgridService } from '../../common/test/overrides';
describe('DBテスト', () => {
let source: DataSource | null = null;
@ -330,6 +331,11 @@ describe('ライセンス割り当て', () => {
);
const service = module.get<UsersService>(UsersService);
overrideSendgridService(service, {
sendMailWithU108: async () => {
return;
},
});
const expiry_date = new NewAllocatedLicenseExpirationDate();
@ -401,6 +407,11 @@ describe('ライセンス割り当て', () => {
);
const service = module.get<UsersService>(UsersService);
overrideSendgridService(service, {
sendMailWithU108: async () => {
return;
},
});
await service.allocateLicense(
makeContext('trackingId', 'requestId'),
@ -478,7 +489,11 @@ describe('ライセンス割り当て', () => {
);
const service = module.get<UsersService>(UsersService);
overrideSendgridService(service, {
sendMailWithU108: async () => {
return;
},
});
const expiry_date = new NewAllocatedLicenseExpirationDate();
await service.allocateLicense(
@ -584,6 +599,11 @@ describe('ライセンス割り当て', () => {
);
const service = module.get<UsersService>(UsersService);
overrideSendgridService(service, {
sendMailWithU108: async () => {
return;
},
});
await service.allocateLicense(
makeContext('trackingId', 'requestId'),
userId,
@ -648,6 +668,11 @@ describe('ライセンス割り当て', () => {
);
const service = module.get<UsersService>(UsersService);
overrideSendgridService(service, {
sendMailWithU108: async () => {
return;
},
});
await service.allocateLicense(
makeContext('trackingId', 'requestId'),
userId,
@ -712,6 +737,11 @@ describe('ライセンス割り当て', () => {
);
const service = module.get<UsersService>(UsersService);
overrideSendgridService(service, {
sendMailWithU108: async () => {
return;
},
});
await service.allocateLicense(
makeContext('trackingId', 'requestId'),
userId,
@ -756,6 +786,11 @@ describe('ライセンス割り当て', () => {
);
const service = module.get<UsersService>(UsersService);
overrideSendgridService(service, {
sendMailWithU108: async () => {
return;
},
});
await expect(
service.allocateLicense(
@ -808,6 +843,11 @@ describe('ライセンス割り当て', () => {
);
const service = module.get<UsersService>(UsersService);
overrideSendgridService(service, {
sendMailWithU108: async () => {
return;
},
});
await expect(
service.allocateLicense(
@ -885,6 +925,11 @@ describe('ライセンス割り当て解除', () => {
);
const service = module.get<UsersService>(UsersService);
overrideSendgridService(service, {
sendMailWithU108: async () => {
return;
},
});
await service.deallocateLicense(
makeContext('trackingId', 'requestId'),
userId,
@ -975,6 +1020,11 @@ describe('ライセンス割り当て解除', () => {
);
const service = module.get<UsersService>(UsersService);
overrideSendgridService(service, {
sendMailWithU108: async () => {
return;
},
});
await expect(
service.deallocateLicense(makeContext('trackingId', 'requestId'), userId),
).rejects.toEqual(
@ -1031,6 +1081,11 @@ describe('ライセンス注文キャンセル', () => {
);
const service = module.get<LicensesService>(LicensesService);
overrideSendgridService(service, {
sendMailWithU106: async () => {
return;
},
});
await service.cancelOrder(
makeContext('trackingId', 'requestId'),
tier2Accounts[0].users[0].external_id,
@ -1066,6 +1121,11 @@ describe('ライセンス注文キャンセル', () => {
);
const service = module.get<LicensesService>(LicensesService);
overrideSendgridService(service, {
sendMailWithU106: async () => {
return;
},
});
await expect(
service.cancelOrder(
makeContext('trackingId', 'requestId'),
@ -1097,6 +1157,11 @@ describe('ライセンス注文キャンセル', () => {
);
const service = module.get<LicensesService>(LicensesService);
overrideSendgridService(service, {
sendMailWithU106: async () => {
return;
},
});
await expect(
service.cancelOrder(
makeContext('trackingId', 'requestId'),

View File

@ -1005,9 +1005,8 @@ export class UsersService {
);
try {
const accountId = (
await this.usersRepository.findUserById(context, userId)
).account_id;
const { external_id: externalId, account_id: accountId } =
await this.usersRepository.findUserById(context, userId);
await this.licensesRepository.allocateLicense(
context,
@ -1015,6 +1014,47 @@ export class UsersService {
newLicenseId,
accountId,
);
// メール送信処理
try {
const { parent_account_id: dealerId } =
await this.accountsRepository.findAccountById(context, accountId);
if (dealerId == null) {
throw new Error(`dealer is null. account_id=${accountId}`);
}
const { company_name: dealerName } =
await this.accountsRepository.findAccountById(context, dealerId);
const { companyName, adminEmails } = await this.getAccountInformation(
context,
accountId,
);
const adb2cUser = await this.adB2cService.getUser(context, externalId);
const { displayName, emailAddress } =
getUserNameAndMailAddress(adb2cUser);
if (emailAddress == null) {
throw new Error(`emailAddress is null. externalId=${externalId}`);
}
// 管理者に割り当てた場合にはTOに管理者のメールアドレスを設定するので、CCには管理者のメールアドレスを設定しない
const ccAdminEmails = adminEmails.filter((x) => x !== emailAddress);
await this.sendgridService.sendMailWithU108(
context,
displayName,
emailAddress,
ccAdminEmails,
companyName,
dealerName,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
// メール送信に関する例外はログだけ出して握りつぶす
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
@ -1197,4 +1237,51 @@ export class UsersService {
);
}
}
/**
* IDを指定して
* @param context
* @param accountId ID
* @returns /
*/
private async getAccountInformation(
context: Context,
accountId: number,
): Promise<{
companyName: string;
adminEmails: string[];
}> {
// アカウントIDから企業名を取得する
const { company_name } = await this.accountsRepository.findAccountById(
context,
accountId,
);
// 管理者一覧を取得
const admins = await this.usersRepository.findAdminUsers(
context,
accountId,
);
const adminExternalIDs = admins.map((x) => x.external_id);
// ADB2Cから管理者IDを元にメールアドレスを取得する
const usersInfo = await this.adB2cService.getUsers(
context,
adminExternalIDs,
);
// 生のAzure AD B2Cのユーザー情報からメールアドレスを抽出する
const adminEmails = usersInfo.map((x) => {
const { emailAddress } = getUserNameAndMailAddress(x);
if (emailAddress == null) {
throw new Error('dealer admin email-address is not found');
}
return emailAddress;
});
return {
companyName: company_name,
adminEmails: adminEmails,
};
}
}

View File

@ -12,6 +12,8 @@ import {
LICENSE_QUANTITY,
PO_NUMBER,
TOP_URL,
USER_EMAIL,
USER_NAME,
} from '../../templates/constants';
@Injectable()
@ -30,6 +32,8 @@ export class SendGridService {
private readonly templateU106Text: string;
private readonly templateU107Html: string;
private readonly templateU107Text: string;
private readonly templateU108Html: string;
private readonly templateU108Text: string;
constructor(private readonly configService: ConfigService) {
this.appDomain = this.configService.getOrThrow<string>('APP_DOMAIN');
@ -86,6 +90,15 @@ export class SendGridService {
path.resolve(__dirname, `../../templates/template_U_107.txt`),
'utf-8',
);
this.templateU108Html = readFileSync(
path.resolve(__dirname, `../../templates/template_U_108.html`),
'utf-8',
);
this.templateU108Text = readFileSync(
path.resolve(__dirname, `../../templates/template_U_108.txt`),
'utf-8',
);
}
}
@ -395,6 +408,61 @@ export class SendGridService {
}
}
/**
* U-108使
* @param context
* @param userName
* @param userMail
* @param customerAdminMails (primary/secondary)
* @param customerAccountName
* @param dealerAccountName
* @returns mail with u108
*/
async sendMailWithU108(
context: Context,
userName: string,
userMail: string,
customerAdminMails: string[],
customerAccountName: string,
dealerAccountName: string,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.sendMailWithU108.name}`,
);
try {
const subject = 'License Assigned Notification [U-108]';
// メールの本文を作成する
const html = this.templateU108Html
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, this.appDomain);
const text = this.templateU108Text
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, this.appDomain);
// メールを送信する
this.sendMail(
context,
[userMail],
customerAdminMails,
this.mailFrom,
subject,
text,
html,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.sendMailWithU108.name}`,
);
}
}
/**
*
* @param context

View File

@ -3,3 +3,5 @@ export const DEALER_NAME = '$DEALER_NAME$';
export const LICENSE_QUANTITY = '$LICENSE_QUANTITY$';
export const PO_NUMBER = '$PO_NUMBER$';
export const TOP_URL = '$TOP_URL$';
export const USER_NAME = '$USER_NAME$';
export const USER_EMAIL = '$USER_EMAIL$';

View File

@ -0,0 +1,80 @@
<html>
<head>
<title>License Assigned Notification [U-108]</title>
</head>
<body>
<div>
<h3>&lt;English&gt;</h3>
<p>Dear $CUSTOMER_NAME$,</p>
<p>
Please be informed that a license has been assigned to the following
user.<br />
- User Name: $USER_NAME$<br />
- Email: $USER_EMAIL$
</p>
<p>
Please log in to ODMS Cloud to verify the license expiration date.<br />
URL: $TOP_URL$
</p>
<p>
If you need support regarding ODMS Cloud, please contact $DEALER_NAME$.
</p>
<p>
If you have received this e-mail in error, please delete this e-mail
from your system.<br />
This is an automatically generated e-mail and this mailbox is not
monitored. Please do not reply.
</p>
</div>
<div>
<h3>&lt;Deutsch&gt;</h3>
<p>Sehr geehrte(r) $CUSTOMER_NAME$,</p>
<p>
Bitte beachten Sie, dass dem folgenden Benutzer eine Lizenz zugewiesen
wurde.<br />
- Nutzername: $USER_NAME$<br />
- Email: $USER_EMAIL$
</p>
<p>
Bitte melden Sie sich bei ODMS Cloud an, um das Ablaufdatum der Lizenz
zu überprüfen.<br />
URL: $TOP_URL$
</p>
<p>
Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich
bitte an $DEALER_NAME$.
</p>
<p>
Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie
diese E-Mail bitte aus Ihrem System.<br />
Dies ist eine automatisch generierte E-Mail und dieses Postfach wird
nicht überwacht. Bitte nicht antworten.
</p>
</div>
<div>
<h3>&lt;Français&gt;</h3>
<p>Chère/Cher $CUSTOMER_NAME$,</p>
<p>
Veuillez être informé qu'une licence a été attribuée à l'utilisateur
suivant.<br />
- Nom d'utilisateur: $USER_NAME$<br />
- Email: $USER_EMAIL$
</p>
<p>
Veuillez vous connecter à ODMS Cloud pour vérifier la date d'expiration
de la licence.<br />
URL: $TOP_URL$
</p>
<p>
Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez
contacter $DEALER_NAME$.
</p>
<p>
Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail
de votre système.<br />
Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres
n'est pas surveillée. Merci de ne pas répondre.
</p>
</div>
</body>
</html>

View File

@ -0,0 +1,47 @@
<English>
Dear $CUSTOMER_NAME$,
Please be informed that a license has been assigned to the following user.
- User Name: $USER_NAME$
- Email: $USER_EMAIL$
Please log in to ODMS Cloud to verify the license expiration date.
URL: $TOP_URL$
If you need support regarding ODMS Cloud, please contact $DEALER_NAME$.
If you have received this e-mail in error, please delete this e-mail from your system.
This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply.
<Deutsch>
Sehr geehrte(r) $CUSTOMER_NAME$,
Bitte beachten Sie, dass dem folgenden Benutzer eine Lizenz zugewiesen wurde.
- Nutzername: $USER_NAME$
- Email: $USER_EMAIL$
Bitte melden Sie sich bei ODMS Cloud an, um das Ablaufdatum der Lizenz zu überprüfen.
URL: $TOP_URL$
Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$.
Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
Dies ist eine automatisch generierte E-Mail und dieses Postfach wird nicht überwacht. Bitte nicht antworten.
<Français>
Chère/Cher $CUSTOMER_NAME$,
Veuillez être informé qu'une licence a été attribuée à l'utilisateur suivant.
- Nom d'utilisateur: $USER_NAME$
- Email: $USER_EMAIL$
Veuillez vous connecter à ODMS Cloud pour vérifier la date d'expiration de la licence.
URL: $TOP_URL$
Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$.
Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre.