diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts index acd0ddc..8d99312 100644 --- a/dictation_server/src/common/test/overrides.ts +++ b/dictation_server/src/common/test/overrides.ts @@ -97,6 +97,41 @@ export const overrideSendgridService = ( customerMail: string, customerAccountName: string, ) => Promise; + sendMailWithU105?: ( + context: Context, + customerMails: string[], + customerAccountName: string, + lisenceCount: number, + poNumber: string, + dealerEmails: string[], + dealerAccountName: string, + ) => Promise; + sendMailWithU106?: ( + context: Context, + customerMails: string[], + customerAccountName: string, + lisenceCount: number, + poNumber: string, + dealerEmails: string[], + dealerAccountName: string, + ) => Promise; + sendMailWithU107?: ( + context: Context, + customerMails: string[], + customerAccountName: string, + lisenceCount: number, + poNumber: string, + dealerEmails: string[], + dealerAccountName: string, + ) => Promise; + sendMailWithU108?: ( + context: Context, + userName: string, + userMail: string, + customerAdminMails: string[], + customerAccountName: string, + dealerAccountName: string, + ) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -131,6 +166,58 @@ export const overrideSendgridService = ( 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, { diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 023d046..d96397b 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -2273,6 +2273,12 @@ describe('issueLicense', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(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); + 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); + 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); + overrideSendgridService(usersService, { + sendMailWithU108: async () => { + return; + }, + }); // アカウントAのライセンスを割り当てる await usersService.allocateLicense( context, diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts index f7d0e49..ad5092a 100644 --- a/dictation_server/src/features/licenses/licenses.service.spec.ts +++ b/dictation_server/src/features/licenses/licenses.service.spec.ts @@ -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); + overrideSendgridService(service, { + sendMailWithU108: async () => { + return; + }, + }); const expiry_date = new NewAllocatedLicenseExpirationDate(); @@ -401,6 +407,11 @@ describe('ライセンス割り当て', () => { ); const service = module.get(UsersService); + overrideSendgridService(service, { + sendMailWithU108: async () => { + return; + }, + }); await service.allocateLicense( makeContext('trackingId', 'requestId'), @@ -478,7 +489,11 @@ describe('ライセンス割り当て', () => { ); const service = module.get(UsersService); - + overrideSendgridService(service, { + sendMailWithU108: async () => { + return; + }, + }); const expiry_date = new NewAllocatedLicenseExpirationDate(); await service.allocateLicense( @@ -584,6 +599,11 @@ describe('ライセンス割り当て', () => { ); const service = module.get(UsersService); + overrideSendgridService(service, { + sendMailWithU108: async () => { + return; + }, + }); await service.allocateLicense( makeContext('trackingId', 'requestId'), userId, @@ -648,6 +668,11 @@ describe('ライセンス割り当て', () => { ); const service = module.get(UsersService); + overrideSendgridService(service, { + sendMailWithU108: async () => { + return; + }, + }); await service.allocateLicense( makeContext('trackingId', 'requestId'), userId, @@ -712,6 +737,11 @@ describe('ライセンス割り当て', () => { ); const service = module.get(UsersService); + overrideSendgridService(service, { + sendMailWithU108: async () => { + return; + }, + }); await service.allocateLicense( makeContext('trackingId', 'requestId'), userId, @@ -756,6 +786,11 @@ describe('ライセンス割り当て', () => { ); const service = module.get(UsersService); + overrideSendgridService(service, { + sendMailWithU108: async () => { + return; + }, + }); await expect( service.allocateLicense( @@ -808,6 +843,11 @@ describe('ライセンス割り当て', () => { ); const service = module.get(UsersService); + overrideSendgridService(service, { + sendMailWithU108: async () => { + return; + }, + }); await expect( service.allocateLicense( @@ -885,6 +925,11 @@ describe('ライセンス割り当て解除', () => { ); const service = module.get(UsersService); + overrideSendgridService(service, { + sendMailWithU108: async () => { + return; + }, + }); await service.deallocateLicense( makeContext('trackingId', 'requestId'), userId, @@ -975,6 +1020,11 @@ describe('ライセンス割り当て解除', () => { ); const service = module.get(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); + 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); + overrideSendgridService(service, { + sendMailWithU106: async () => { + return; + }, + }); await expect( service.cancelOrder( makeContext('trackingId', 'requestId'), @@ -1097,6 +1157,11 @@ describe('ライセンス注文キャンセル', () => { ); const service = module.get(LicensesService); + overrideSendgridService(service, { + sendMailWithU106: async () => { + return; + }, + }); await expect( service.cancelOrder( makeContext('trackingId', 'requestId'), diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 16e1339..2ae167b 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -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, + }; + } } diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index e20fd2b..289b14a 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -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('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 { + 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 diff --git a/dictation_server/src/templates/constants.ts b/dictation_server/src/templates/constants.ts index b715abb..8ff9d75 100644 --- a/dictation_server/src/templates/constants.ts +++ b/dictation_server/src/templates/constants.ts @@ -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$'; diff --git a/dictation_server/src/templates/template_U_108.html b/dictation_server/src/templates/template_U_108.html new file mode 100644 index 0000000..ad61961 --- /dev/null +++ b/dictation_server/src/templates/template_U_108.html @@ -0,0 +1,80 @@ + + + License Assigned Notification [U-108] + + +
+

<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. +

+
+ + diff --git a/dictation_server/src/templates/template_U_108.txt b/dictation_server/src/templates/template_U_108.txt new file mode 100644 index 0000000..8b695ca --- /dev/null +++ b/dictation_server/src/templates/template_U_108.txt @@ -0,0 +1,47 @@ + + +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. + + + +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. + + + +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. \ No newline at end of file