Merged PR 644: Dictation Workflow完了通知 [U-117] の実装

## 概要
[Task3313: Dictation Workflow完了通知 [U-117] の実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3313)

- 文字起こし完了時にメール送信する機能を実装しました。
- npm run formatで変更あった箇所も入っています。

## レビューポイント
- SendGridServiceのIFを「こうしたほうがいいかも」とかあれば。
- メール送信に必要な内容取得で効率的にできそうな部分ないか?

## UIの変更
- なし

## 動作確認状況
- ローカルでnpm run testが通ることを確認
- ローカルでメール送信されることを確認
This commit is contained in:
Kentaro Fukunaga 2023-12-21 06:49:30 +00:00
parent a6f56d71ee
commit 9baae2d2dc
9 changed files with 330 additions and 7 deletions

View File

@ -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],

View File

@ -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<User> {
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

View File

@ -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();

View File

@ -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 };
}
};

View File

@ -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<string>('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<void> {
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

View File

@ -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<User> {
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

View File

@ -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$';

View File

@ -0,0 +1,58 @@
<html>
<head>
<title>Transcription Completion Notification [U-117]</title>
</head>
<body>
<div>
<h3>&lt;English&gt;</h3>
<p>Dear $AUTHOR_NAME$,</p>
<p>
The transcription of the dictation you uploaded to ODMS Cloud has been completed.<br />
- Dictation file name: $FILE_NAME$<br />
- Transcriptionist name: $TYPIST_NAME$
</p>
<p>
If you need support regarding ODMS Cloud, please contact $PRIMARY_ADMIN_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) $AUTHOR_NAME$,</p>
<p>
Die Transkription des Diktats, das Sie in die ODMS Cloud hochgeladen haben, ist abgeschlossen.<br />
- Name der Diktatdatei: $FILE_NAME$<br />
- Name des Transkriptionisten: $TYPIST_NAME$
</p>
<p>
Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $PRIMARY_ADMIN_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 $AUTHOR_NAME$,</p>
<p>
La transcription de la dictée que vous avez téléchargée sur ODMS Cloud est terminée.<br />
- Nom du fichier de dictée: $FILE_NAME$<br />
- Nom du transcriptionniste: $TYPIST_NAME$
</p>
<p>
Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $PRIMARY_ADMIN_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,38 @@
<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.