diff --git a/dictation_server/src/features/tasks/tasks.module.ts b/dictation_server/src/features/tasks/tasks.module.ts index ce184ae..c3e10e0 100644 --- a/dictation_server/src/features/tasks/tasks.module.ts +++ b/dictation_server/src/features/tasks/tasks.module.ts @@ -6,14 +6,18 @@ import { TasksRepositoryModule } from '../../repositories/tasks/tasks.repository import { AdB2cModule } from '../../gateways/adb2c/adb2c.module'; import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module'; import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module'; +import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; +import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module'; @Module({ imports: [ + AccountsRepositoryModule, UsersRepositoryModule, UserGroupsRepositoryModule, TasksRepositoryModule, AdB2cModule, NotificationhubModule, + SendGridModule, ], providers: [TasksService], controllers: [TasksController], diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts index d1cc1e7..d56055b 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -33,15 +33,20 @@ import { NotificationhubService } from '../../gateways/notificationhub/notificat import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service'; import { Context } from '../../common/log'; import { User } from '../../repositories/users/entity/user.entity'; +import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; +import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; +import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; @Injectable() export class TasksService { private readonly logger = new Logger(TasksService.name); constructor( + private readonly accountsRepository: AccountsRepositoryService, private readonly taskRepository: TasksRepositoryService, private readonly usersRepository: UsersRepositoryService, private readonly userGroupsRepositoryService: UserGroupsRepositoryService, private readonly adB2cService: AdB2cService, + private readonly sendgridService: SendGridService, private readonly notificationhubService: NotificationhubService, ) {} @@ -357,17 +362,103 @@ export class TasksService { this.checkin.name } | params: { audioFileId: ${audioFileId}, externalId: ${externalId} };`, ); - const { id } = await this.usersRepository.findUserByExternalId( + const user = await this.usersRepository.findUserByExternalId( context, externalId, ); - return await this.taskRepository.checkin( + await this.taskRepository.checkin( context, audioFileId, - id, + user.id, TASK_STATUS.IN_PROGRESS, ); + + // メール送信処理 + try { + // タスク情報の取得 + const task = await this.taskRepository.getTaskAndAudioFile( + context, + audioFileId, + user.account_id, + [TASK_STATUS.FINISHED], + ); + if (!task) { + throw new Error( + `task not found. audioFileId: ${audioFileId}. account_id: ${user.account_id}`, + ); + } + + // author情報の取得 + if (!task.file?.author_id) { + throw new Error( + `author_id not found. audioFileId: ${audioFileId}. account_id: ${user.account_id}`, + ); + } + const { external_id: authorExternalId } = + await this.usersRepository.findUserByAuthorId( + context, + task.file.author_id, + user.account_id, + ); + + // プライマリ管理者を取得 + const { external_id: primaryAdminExternalId } = + await this.getPrimaryAdminUser(context, user.account_id); + + // ADB2C情報を取得する + const usersInfo = await this.adB2cService.getUsers(context, [ + externalId, + authorExternalId, + primaryAdminExternalId, + ]); + + // メール送信に必要な情報を取得 + const author = usersInfo.find((x) => x.id === authorExternalId); + if (!author) { + throw new Error(`author not found. id=${authorExternalId}`); + } + const { displayName: authorName, emailAddress: authorEmail } = + getUserNameAndMailAddress(author); + if (!authorEmail) { + throw new Error(`author email not found. id=${authorExternalId}`); + } + + const typist = usersInfo.find((x) => x.id === externalId); + if (!typist) { + throw new Error(`typist not found. id=${externalId}`); + } + const { displayName: typistName, emailAddress: typistEmail } = + getUserNameAndMailAddress(typist); + if (!typistEmail) { + throw new Error(`typist email not found. id=${externalId}`); + } + + const primaryAdmin = usersInfo.find( + (x) => x.id === primaryAdminExternalId, + ); + if (!primaryAdmin) { + throw new Error( + `primary admin not found. id=${primaryAdminExternalId}`, + ); + } + const { displayName: primaryAdminName } = + getUserNameAndMailAddress(primaryAdmin); + + // メール送信 + this.sendgridService.sendMailWithU117( + context, + authorEmail, + typistEmail, + authorName, + task.file.file_name.replace('.zip', ''), + typistName, + primaryAdminName, + ); + } catch (e) { + // メール送信に関する例外はログだけ出して握りつぶす + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + } } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); if (e instanceof Error) { @@ -400,6 +491,26 @@ export class TasksService { ); } } + + private async getPrimaryAdminUser( + context: Context, + accountId: number, + ): Promise { + const accountInfo = await this.accountsRepository.findAccountById( + context, + accountId, + ); + if (!accountInfo || !accountInfo.primary_admin_user_id) { + throw new Error(`account or primary admin not found. id=${accountId}`); + } + + const primaryAdmin = await this.usersRepository.findUserById( + context, + accountInfo.primary_admin_user_id, + ); + + return primaryAdmin; + } /** * 指定した音声ファイルに紐づくタスクをキャンセルする * @param audioFileId diff --git a/dictation_server/src/features/tasks/test/tasks.service.mock.ts b/dictation_server/src/features/tasks/test/tasks.service.mock.ts index 5d5d97c..94d3e7d 100644 --- a/dictation_server/src/features/tasks/test/tasks.service.mock.ts +++ b/dictation_server/src/features/tasks/test/tasks.service.mock.ts @@ -15,6 +15,8 @@ import { Assignee } from '../types/types'; import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity'; import { NotificationhubService } from '../../../gateways/notificationhub/notificationhub.service'; import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service'; +import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service'; +import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service'; export type TasksRepositoryMockValue = { getTasksFromAccountId: @@ -84,6 +86,12 @@ export const makeTasksServiceMock = async ( return makeNotificationhubServiceMock( notificationhubServiceMockValue, ); + // メール送信でしか利用しておらず、テストする必要がないが、依存関係解決のため空オブジェクトを定義しておく。 + case AccountsRepositoryService: + return {}; + // メール送信でしか利用しておらず、テストする必要がないが、依存関係解決のため空オブジェクトを定義しておく。 + case SendGridService: + return {}; } }) .compile(); diff --git a/dictation_server/src/gateways/adb2c/utils/utils.ts b/dictation_server/src/gateways/adb2c/utils/utils.ts index dfeb4c7..e1b640e 100644 --- a/dictation_server/src/gateways/adb2c/utils/utils.ts +++ b/dictation_server/src/gateways/adb2c/utils/utils.ts @@ -1,5 +1,5 @@ -import { ADB2C_SIGN_IN_TYPE } from "../../../constants"; -import { AdB2cUser } from "../types/types"; +import { ADB2C_SIGN_IN_TYPE } from '../../../constants'; +import { AdB2cUser } from '../types/types'; export const isPromiseRejectedResult = ( data: unknown, @@ -19,4 +19,4 @@ export const getUserNameAndMailAddress = (user: AdB2cUser) => { (identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EMAILADDRESS, )?.issuerAssignedId; return { displayName, emailAddress }; -} \ No newline at end of file +}; diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index 4b4c642..a5b3c92 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -7,14 +7,17 @@ import { Context } from '../../common/log'; import { readFileSync } from 'node:fs'; import path from 'node:path'; import { + PRIMARY_ADMIN_NAME, + AUTHOR_NAME, CUSTOMER_NAME, DEALER_NAME, + FILE_NAME, LICENSE_QUANTITY, PO_NUMBER, - PRIMARY_ADMIN_NAME, TOP_URL, USER_EMAIL, USER_NAME, + TYPIST_NAME, } from '../../templates/constants'; @Injectable() @@ -39,6 +42,8 @@ export class SendGridService { private readonly templateU109Text: string; private readonly templateU111Html: string; private readonly templateU111Text: string; + private readonly templateU117Html: string; + private readonly templateU117Text: string; constructor(private readonly configService: ConfigService) { this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); @@ -121,6 +126,15 @@ export class SendGridService { path.resolve(__dirname, `../../templates/template_U_111.txt`), 'utf-8', ); + + this.templateU117Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_117.html`), + 'utf-8', + ); + this.templateU117Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_117.txt`), + 'utf-8', + ); } } @@ -588,10 +602,66 @@ export class SendGridService { } } + /** + * U-117のテンプレートを使用したメールを送信する + * @param context + * @param authorEmail 文字起こしファイルのAuthorのメールアドレス + * @param typistEmail 文字起こしを行ったTypistのメールアドレス + * @param authorName 文字起こしファイルのAuthorの名前 + * @param fileName 文字起こしファイルのファイル名 + * @param typistName 文字起こしを行ったTypistの名前 + * @param adminName アカウント管理者の名前(プライマリ) + * @returns mail with u117 + */ + async sendMailWithU117( + context: Context, + authorEmail: string, + typistEmail: string, + authorName: string, + fileName: string, + typistName: string, + adminName: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU117.name}`, + ); + try { + const subject = 'Transcription Completion Notification [U-117]'; + + // メールの本文を作成する + const html = this.templateU117Html + .replaceAll(AUTHOR_NAME, authorName) + .replaceAll(FILE_NAME, fileName) + .replaceAll(TYPIST_NAME, typistName) + .replaceAll(PRIMARY_ADMIN_NAME, adminName); + const text = this.templateU117Text + .replaceAll(AUTHOR_NAME, authorName) + .replaceAll(FILE_NAME, fileName) + .replaceAll(TYPIST_NAME, typistName) + .replaceAll(PRIMARY_ADMIN_NAME, adminName); + + // メールを送信する + this.sendMail( + context, + [authorEmail, typistEmail], + [], + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU117.name}`, + ); + } + } + /** * メールを送信する * @param context * @param to + * @param cc * @param from * @param subject * @param text diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index ca46c20..2fb67af 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -175,6 +175,37 @@ export class UsersRepositoryService { return user; } + /** + * AuthorIDをもとにユーザーを取得します。 + * AuthorIDがセットされていない場合や、ユーザーが存在しない場合はエラーを返します。 + * @param context + * @param authorId 検索対象のAuthorID + * @param accountId 検索対象のアカウントID + * @returns user by author id + */ + async findUserByAuthorId( + context: Context, + authorId: string, + accountId: number, + ): Promise { + if (!authorId) { + throw new Error('authorId is not set.'); + } + + const user = await this.dataSource.getRepository(User).findOne({ + where: { + author_id: authorId, + account_id: accountId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (!user) { + throw new UserNotFoundError(`User not Found.`); + } + return user; + } + /** * 指定したアカウントIDを持つアカウントのプライマリ管理者とセカンダリ管理者を取得する * @param context context diff --git a/dictation_server/src/templates/constants.ts b/dictation_server/src/templates/constants.ts index e7c95b8..09bf5e5 100644 --- a/dictation_server/src/templates/constants.ts +++ b/dictation_server/src/templates/constants.ts @@ -6,3 +6,6 @@ export const TOP_URL = '$TOP_URL$'; export const PRIMARY_ADMIN_NAME = '$PRIMARY_ADMIN_NAME$'; export const USER_NAME = '$USER_NAME$'; export const USER_EMAIL = '$USER_EMAIL$'; +export const AUTHOR_NAME = '$AUTHOR_NAME$'; +export const FILE_NAME = '$FILE_NAME$'; +export const TYPIST_NAME = '$TYPIST_NAME$'; diff --git a/dictation_server/src/templates/template_U_117.html b/dictation_server/src/templates/template_U_117.html new file mode 100644 index 0000000..89fa0ba --- /dev/null +++ b/dictation_server/src/templates/template_U_117.html @@ -0,0 +1,58 @@ + + + + Transcription Completion Notification [U-117] + + + +
+

<English>

+

Dear $AUTHOR_NAME$,

+

+ The transcription of the dictation you uploaded to ODMS Cloud has been completed.
+ - Dictation file name: $FILE_NAME$
+ - Transcriptionist name: $TYPIST_NAME$ +

+

+ If you need support regarding ODMS Cloud, please contact $PRIMARY_ADMIN_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) $AUTHOR_NAME$,

+

+ Die Transkription des Diktats, das Sie in die ODMS Cloud hochgeladen haben, ist abgeschlossen.
+ - Name der Diktatdatei: $FILE_NAME$
+ - Name des Transkriptionisten: $TYPIST_NAME$ +

+

+ Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $PRIMARY_ADMIN_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 $AUTHOR_NAME$,

+

+ La transcription de la dictée que vous avez téléchargée sur ODMS Cloud est terminée.
+ - Nom du fichier de dictée: $FILE_NAME$
+ - Nom du transcriptionniste: $TYPIST_NAME$ +

+

+ Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $PRIMARY_ADMIN_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 diff --git a/dictation_server/src/templates/template_U_117.txt b/dictation_server/src/templates/template_U_117.txt new file mode 100644 index 0000000..9d84ae3 --- /dev/null +++ b/dictation_server/src/templates/template_U_117.txt @@ -0,0 +1,38 @@ + + +Dear $AUTHOR_NAME$, + +The transcription of the dictation you uploaded to ODMS Cloud has been completed. + - Dictation file name: $FILE_NAME$ + - Transcriptionist name: $TYPIST_NAME$ + +If you need support regarding ODMS Cloud, please contact $PRIMARY_ADMIN_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) $AUTHOR_NAME$, + +Die Transkription des Diktats, das Sie in die ODMS Cloud hochgeladen haben, ist abgeschlossen. + - Name der Diktatdatei: $FILE_NAME$ + - Name des Transkriptionisten: $TYPIST_NAME$ + +Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $PRIMARY_ADMIN_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 $AUTHOR_NAME$, + +La transcription de la dictée que vous avez téléchargée sur ODMS Cloud est terminée. + - Nom du fichier de dictée: $FILE_NAME$ + - Nom du transcriptionniste: $TYPIST_NAME$ + +Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $PRIMARY_ADMIN_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