saito.k f1b75a7ff0 Merged PR 921: API修正
## 概要
[Task4478: API修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/4478)

- 元PBI or タスクへのリンク(内容・目的などはそちらにあるはず)
- 何をどう変更したか、追加したライブラリなど
- このPull Requestでの対象/対象外
- 影響範囲(他の機能にも影響があるか)

## レビューポイント
- 特にレビューしてほしい箇所
- 軽微なものや自明なものは記載不要
- 修正範囲が大きい場合などに記載
- 全体的にや仕様を満たしているか等は本当に必要な時のみ記載
- 修正箇所がほかの機能に影響していないか

## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## クエリの変更
- Repositoryを変更し、クエリが変更された場合は変更内容を確認する
- Before/Afterのクエリ
- クエリ置き場

## 動作確認状況
- ローカルで確認、develop環境で確認など
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - どのケースに対してどのような手段でデグレがないことを担保しているか

## 補足
- 相談、参考資料などがあれば
2024-09-18 01:35:28 +00:00

1051 lines
33 KiB
TypeScript

import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
import { Assignee, PostDeleteTaskRequest, Task } from './types/types';
import { Task as TaskEntity } from '../../repositories/tasks/entity/task.entity';
import { createTasks } from './types/convert';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import {
SortDirection,
TaskListSortableAttribute,
} from '../../common/types/sort';
import {
ADMIN_ROLES,
MANUAL_RECOVERY_REQUIRED,
TASK_STATUS,
TIERS,
USER_LICENSE_STATUS,
USER_ROLES,
} from '../../constants';
import {
AdB2cService,
Adb2cTooManyRequestsError,
} from '../../gateways/adb2c/adb2c.service';
import { AdB2cUser } from '../../gateways/adb2c/types/types';
import { CheckoutPermission } from '../../repositories/checkout_permissions/entity/checkout_permission.entity';
import {
AccountNotMatchError,
AlreadyHasInProgressTaskError,
CheckoutPermissionNotFoundError,
StatusNotMatchError,
TaskAuthorIdNotMatchError,
TasksNotFoundError,
TypistUserGroupNotFoundError,
TypistUserNotFoundError,
TypistUserNotMatchError,
} from '../../repositories/tasks/errors/types';
import { Roles } from '../../common/types/role';
import { InvalidRoleError } from './errors/types';
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
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';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { AccountNotFoundError } from '../../repositories/accounts/errors/types';
import {
LicenseExpiredError,
LicenseNotAllocatedError,
} from '../../repositories/licenses/errors/types';
import { LicensesRepositoryService } from '../../repositories/licenses/licenses.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,
private readonly blobStorageService: BlobstorageService,
private readonly licensesRepository: LicensesRepositoryService,
) {}
async getTasks(
context: Context,
userId: string,
roles: Roles[],
offset: number,
limit: number,
status?: string[],
paramName?: TaskListSortableAttribute,
direction?: SortDirection,
): Promise<{ tasks: Task[]; total: number }> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.getTasks.name} | params: { ` +
`userId: ${userId}, ` +
`roles: ${roles}, ` +
`offset: ${offset},` +
`limit: ${limit}, ` +
`status: ${status}, ` +
`paramName: ${paramName}, ` +
`direction: ${direction} };`,
);
// パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える
const defaultParamName: TaskListSortableAttribute = 'JOB_NUMBER';
const defaultDirection: SortDirection = 'ASC';
// statusが省略された場合のデフォルト値: 全てのステータス
const defaultStatus = Object.values(TASK_STATUS);
try {
const { account_id, author_id } =
await this.usersRepository.findUserByExternalId(context, userId);
if (roles.includes(ADMIN_ROLES.ADMIN)) {
const result = await this.taskRepository.getTasksFromAccountId(
context,
account_id,
offset,
limit,
paramName ?? defaultParamName,
direction ?? defaultDirection,
status ?? defaultStatus,
);
// B2Cからユーザー名を取得する
const b2cUsers = await this.getB2cUsers(
context,
result.tasks,
result.permissions,
);
const tasks = createTasks(result.tasks, result.permissions, b2cUsers);
return { tasks: tasks, total: result.count };
}
if (roles.includes(USER_ROLES.AUTHOR)) {
// API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
if (!author_id) {
throw new Error('AuthorID not found');
}
const result =
await this.taskRepository.getTasksFromAuthorIdAndAccountId(
context,
author_id,
account_id,
offset,
limit,
paramName ?? defaultParamName,
direction ?? defaultDirection,
status ?? defaultStatus,
);
// B2Cからユーザー名を取得する
const b2cUsers = await this.getB2cUsers(
context,
result.tasks,
result.permissions,
);
const tasks = createTasks(result.tasks, result.permissions, b2cUsers);
return { tasks: tasks, total: result.count };
}
if (roles.includes(USER_ROLES.TYPIST)) {
const result = await this.taskRepository.getTasksFromTypistRelations(
context,
userId,
offset,
limit,
paramName ?? defaultParamName,
direction ?? defaultDirection,
status ?? defaultStatus,
);
// B2Cからユーザー名を取得する
const b2cUsers = await this.getB2cUsers(
context,
result.tasks,
result.permissions,
);
const tasks = createTasks(result.tasks, result.permissions, b2cUsers);
return { tasks: tasks, total: result.count };
}
throw new Error(`invalid roles: ${roles.join(',')}`);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
if (e.constructor === Adb2cTooManyRequestsError) {
throw new HttpException(
makeErrorResponse('E000301'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getTasks.name}`,
);
}
}
/**
* 完了したタスクの次のタスクを取得します
* @param context
* @param externalId
* @param fileId
* @returns next task
*/
async getNextTask(
context: Context,
externalId: string,
fileId: number,
): Promise<number | undefined> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getNextTask.name
} | params: { externalId: ${externalId}, fileId: ${fileId} };`,
);
try {
const { account_id: accountId, id } =
await this.usersRepository.findUserByExternalId(context, externalId);
// タスク一覧を取得する
const tasks = await this.taskRepository.getSortedTasks(
context,
accountId,
id,
fileId,
);
// 指定タスクのインデックスを取得する
const targetTaskIndex = tasks.findIndex(
(x) => x.audio_file_id === fileId,
);
// 指定したタスクが見つからない場合はエラーとする(リポジトリからは必ず取得できる想定)
if (targetTaskIndex === -1) {
throw new TasksNotFoundError(`task not found: ${fileId}`);
}
// ソート順に並んだタスクについて、指定した完了済みタスクの次のタスクを取得する
let nextTaskIndex = targetTaskIndex + 1;
// 次のタスクがない場合は先頭のタスクを返す
if (tasks.length - 1 < nextTaskIndex) {
nextTaskIndex = 0;
}
const nextTask = tasks[nextTaskIndex];
// 先頭のタスクが指定した完了済みタスクの場合は次のタスクがないためundefinedを返す
return nextTask.audio_file_id === fileId
? undefined
: nextTask.audio_file_id;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TasksNotFoundError:
throw new HttpException(
makeErrorResponse('E010603'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getNextTask.name}`,
);
}
}
/**
* 指定した音声ファイルに紐づくタスクをcheckoutする
* @param audioFileId
* @param roles
* @param externalId
* @returns checkout
*/
async checkout(
context: Context,
audioFileId: number,
roles: Roles[],
externalId: string,
): Promise<void> {
try {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.checkout.name
} | params: { audioFileId: ${audioFileId}, roles: ${roles}, externalId: ${externalId} };`,
);
const { id, account_id, author_id, account } =
await this.usersRepository.findUserByExternalId(context, externalId);
if (!account) {
throw new AccountNotFoundError('account not found.');
}
// 第五階層のみチェック
if (account.tier === TIERS.TIER5) {
// ライセンスが有効でない場合、エラー
const { state } = await this.licensesRepository.getLicenseState(
context,
id,
);
if (state === USER_LICENSE_STATUS.EXPIRED) {
throw new LicenseExpiredError('license is expired.');
}
if (state === USER_LICENSE_STATUS.UNALLOCATED) {
throw new LicenseNotAllocatedError('license is not allocated.');
}
}
if (roles.includes(USER_ROLES.AUTHOR)) {
// API実行者がAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
if (!author_id) {
throw new Error('AuthorID not found');
}
await this.taskRepository.getTaskFromAudioFileId(
context,
audioFileId,
account_id,
author_id,
);
return;
}
if (roles.includes(USER_ROLES.TYPIST)) {
return await this.taskRepository.checkout(
context,
audioFileId,
account_id,
id,
[TASK_STATUS.UPLOADED, TASK_STATUS.PENDING, TASK_STATUS.IN_PROGRESS],
);
}
throw new InvalidRoleError(`invalid roles: ${roles.join(',')}`);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case LicenseExpiredError:
throw new HttpException(
makeErrorResponse('E010805'),
HttpStatus.BAD_REQUEST,
);
case LicenseNotAllocatedError:
throw new HttpException(
makeErrorResponse('E010812'),
HttpStatus.BAD_REQUEST,
);
case CheckoutPermissionNotFoundError:
case TaskAuthorIdNotMatchError:
case InvalidRoleError:
throw new HttpException(
makeErrorResponse('E010602'),
HttpStatus.BAD_REQUEST,
);
case TasksNotFoundError:
throw new HttpException(
makeErrorResponse('E010601'),
HttpStatus.NOT_FOUND,
);
case AccountNotMatchError:
case StatusNotMatchError:
case AlreadyHasInProgressTaskError:
throw new HttpException(
makeErrorResponse('E010601'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.checkout.name}`,
);
}
}
/**
* 指定した音声ファイルに紐づくタスクをcheckinする
* @param audioFileId
* @param externalId
* @returns checkin
*/
async checkin(
context: Context,
audioFileId: number,
externalId: string,
): Promise<void> {
try {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.checkin.name
} | params: { audioFileId: ${audioFileId}, externalId: ${externalId} };`,
);
const user = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
await this.taskRepository.checkin(
context,
audioFileId,
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,
notification: authorNotification,
} = 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,
]);
// メール送信に必要な情報を取得
// Author通知ON/OFF関わらずAuthor名は必要なため、情報の取得は行う
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 } = getUserNameAndMailAddress(typist);
const primaryAdmin = usersInfo.find(
(x) => x.id === primaryAdminExternalId,
);
if (!primaryAdmin) {
throw new Error(
`primary admin not found. id=${primaryAdminExternalId}`,
);
}
const { displayName: primaryAdminName } =
getUserNameAndMailAddress(primaryAdmin);
// メール送信
await this.sendgridService.sendMailWithU117(
context,
authorNotification ? authorEmail : null,
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) {
switch (e.constructor) {
case TasksNotFoundError:
throw new HttpException(
makeErrorResponse('E010603'),
HttpStatus.NOT_FOUND,
);
case StatusNotMatchError:
case TypistUserNotMatchError:
throw new HttpException(
makeErrorResponse('E010601'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.checkin.name}`,
);
}
}
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
* @param externalId
* @param role
* @returns cancel
*/
async cancel(
context: Context,
audioFileId: number,
externalId: string,
role: Roles[],
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.cancel.name
} | params: { audioFileId: ${audioFileId}, externalId: ${externalId}, role: ${role} };`,
);
let user: User;
try {
// ユーザー取得
user = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.cancel.name}`);
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// roleにAdminが含まれていれば、文字起こし担当でなくてもキャンセルできるため、ユーザーIDは指定しない
await this.taskRepository.cancel(
context,
audioFileId,
[TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING],
user.account_id,
role.includes(ADMIN_ROLES.ADMIN) ? undefined : user.id,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TasksNotFoundError:
throw new HttpException(
makeErrorResponse('E010603'),
HttpStatus.NOT_FOUND,
);
case StatusNotMatchError:
case TypistUserNotMatchError:
throw new HttpException(
makeErrorResponse('E010601'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
try {
// キャンセルしたタスクに自動ルーティングを行う
const { typistGroupIds, typistIds } =
await this.taskRepository.autoRouting(
context,
audioFileId,
user.account_id,
);
// 通知を送信する
await this.sendNotify(
context,
typistIds,
typistGroupIds,
audioFileId,
user.account_id,
);
} catch (e) {
// 処理の本筋はタスクキャンセルのため自動ルーティングに失敗してもエラーにしない
this.logger.error(
`[${context.getTrackingId()}] Automatic routing or notification failed.`,
);
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
} finally {
this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.cancel.name}`);
}
}
/**
* 指定した音声ファイルに紐づくタスクをsuspendする
* @param audioFileId
* @param externalId
* @returns suspend
*/
async suspend(
context: Context,
audioFileId: number,
externalId: string,
): Promise<void> {
try {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.suspend.name
} | params: { audioFileId: ${audioFileId}, externalId: ${externalId} };`,
);
const { id } = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
return await this.taskRepository.suspend(
context,
audioFileId,
id,
TASK_STATUS.IN_PROGRESS,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TasksNotFoundError:
throw new HttpException(
makeErrorResponse('E010603'),
HttpStatus.NOT_FOUND,
);
case StatusNotMatchError:
case TypistUserNotMatchError:
throw new HttpException(
makeErrorResponse('E010601'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.suspend.name}`,
);
}
}
/**
* 指定した音声ファイルに紐づくタスクをbackupする
* @param context
* @param audioFileId
* @param externalId
* @returns backup
*/
async backup(
context: Context,
audioFileId: number,
externalId: string,
): Promise<void> {
try {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.backup.name
} | params: { audioFileId: ${audioFileId}, externalId: ${externalId} };`,
);
const { account_id: accountId } =
await this.usersRepository.findUserByExternalId(context, externalId);
await this.taskRepository.backup(context, accountId, audioFileId, [
TASK_STATUS.FINISHED,
TASK_STATUS.BACKUP,
]);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TasksNotFoundError:
throw new HttpException(
makeErrorResponse('E010603'),
HttpStatus.NOT_FOUND,
);
case StatusNotMatchError:
throw new HttpException(
makeErrorResponse('E010601'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.backup.name}`);
}
}
private async getB2cUsers(
context: Context,
tasks: TaskEntity[],
permissions: CheckoutPermission[],
): Promise<AdB2cUser[]> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.getB2cUsers.name
} | params: { tasks: ${tasks}, permissions: ${permissions} };`,
);
// 割り当て候補の外部IDを列挙
const assigneesExternalIds = permissions.flatMap((permission) =>
permission.user ? [permission.user.external_id] : [],
);
// 割り当てられているタイピストの外部IDを列挙
const typistExternalIds = tasks.flatMap((task) =>
task.typist_user ? [task.typist_user.external_id] : [],
);
//重複をなくす
const distinctedExternalIds = [
...new Set(assigneesExternalIds.concat(typistExternalIds)),
];
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.getB2cUsers.name}`,
);
// B2Cからユーザー名を取得する
return await this.adB2cService.getUsers(context, distinctedExternalIds);
}
/**
* 文字起こし候補を変更する
* @param audioFileId
* @param assignees
* @returns checkout permission
*/
async changeCheckoutPermission(
context: Context,
audioFileId: number,
assignees: Assignee[],
externalId: string,
role: Roles[],
): Promise<void> {
try {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.changeCheckoutPermission.name
} | params: { audioFileId: ${audioFileId}, assignees: ${assignees}, externalId: ${externalId}, role: ${role} };`,
);
const { author_id, account_id } =
await this.usersRepository.findUserByExternalId(context, externalId);
// RoleがAuthorで、AuthorIDが存在しないことは想定外のため、エラーとする
if (role.includes(USER_ROLES.AUTHOR) && !author_id) {
throw new Error('AuthorID not found');
}
await this.taskRepository.changeCheckoutPermission(
context,
audioFileId,
author_id ?? undefined,
account_id,
role,
assignees,
);
// すべての割り当て候補ユーザーを取得する
const assigneesGroupIds = assignees
.filter((assignee) => assignee.typistGroupId)
.flatMap((assignee) =>
assignee.typistGroupId ? [assignee.typistGroupId] : [],
);
const assigneesUserIds = assignees
.filter((assignee) => assignee.typistUserId)
.flatMap((assignee) =>
assignee.typistUserId ? [assignee.typistUserId] : [],
);
// 通知を送信する
await this.sendNotify(
context,
assigneesUserIds,
assigneesGroupIds,
audioFileId,
account_id,
);
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case TypistUserNotFoundError:
case TypistUserGroupNotFoundError:
throw new HttpException(
makeErrorResponse('E010204'),
HttpStatus.BAD_REQUEST,
);
case TasksNotFoundError:
throw new HttpException(
makeErrorResponse('E010601'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${
this.changeCheckoutPermission.name
}`,
);
}
}
/**
* 指定した音声ファイルに紐づくタスクを削除します
* @param context
* @param externalId 実行ユーザーの外部ID
* @param audioFileId 削除対象のタスクのaudio_file_id
* @returns task
*/
async deleteTask(
context: Context,
externalId: string,
audioFileId: number,
): Promise<void> {
try {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${
this.deleteTask.name
} | params: { externalId: ${externalId}, audioFileId: ${audioFileId} };`,
);
// 実行ユーザーの情報を取得する
const user = await this.usersRepository.findUserByExternalId(
context,
externalId,
);
const account = user.account;
if (!account) {
throw new Error(`account not found. externalId: ${externalId}`);
}
// 削除対象の音声ファイル情報を取得する
const task = await this.taskRepository.getTaskAndAudioFile(
context,
audioFileId,
user.account_id,
Object.values(TASK_STATUS),
);
const targetFileName = task.file?.raw_file_name;
if (!targetFileName) {
throw new Error(`target file not found. audioFileId: ${audioFileId}`);
}
// DBからタスクと紐づくデータを削除する
await this.taskRepository.deleteTask(context, user.id, audioFileId);
// Blob削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行するため、try-catchで囲む
try {
// BlobStorageから音声ファイルを削除する
await this.blobStorageService.deleteFile(
context,
account.id,
account.country,
targetFileName,
);
} catch (e) {
// Blob削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行
this.logger.log(`[${context.getTrackingId()}] ${e}`);
this.logger.log(
`${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete Blob: accountId: ${
account.id
}, fileName: ${targetFileName}`,
);
}
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case StatusNotMatchError:
throw new HttpException(
makeErrorResponse('E010601'),
HttpStatus.BAD_REQUEST,
);
case TaskAuthorIdNotMatchError:
throw new HttpException(
makeErrorResponse('E010602'),
HttpStatus.BAD_REQUEST,
);
case TasksNotFoundError:
throw new HttpException(
makeErrorResponse('E010603'),
HttpStatus.BAD_REQUEST,
);
default:
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
throw new HttpException(
makeErrorResponse('E009999'),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} finally {
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.deleteTask.name}`,
);
}
}
// 通知を送信するプライベートメソッド
private async sendNotify(
context: Context,
typistUserIds: number[],
typistGroupIds: number[],
audioFileId: number,
accountId: number,
): Promise<void> {
this.logger.log(
`[IN] [${context.getTrackingId()}] ${this.sendNotify.name} | params: { ` +
`typistUserIds: ${typistUserIds}, ` +
`typistGroupIds: ${typistGroupIds}, ` +
`audioFileId: ${audioFileId}, ` +
`accountId: ${accountId} };`,
);
const groupMembers =
await this.userGroupsRepositoryService.getGroupMembersFromGroupIds(
context,
typistGroupIds,
);
// 重複のない割り当て候補ユーザーID一覧を取得する
const distinctUserIds = [
...new Set([...typistUserIds, ...groupMembers.map((x) => x.user_id)]),
];
// 割り当てられたユーザーがいない場合は通知不要
if (distinctUserIds.length === 0) {
this.logger.log(`[${context.getTrackingId()}] No user assigned.`);
return;
}
// タグを生成
const tags = distinctUserIds.map((x) => `user_${x}`);
this.logger.log(`[${context.getTrackingId()}] tags: ${tags}`);
// 通知内容に含む音声ファイル情報を取得
const { file } = await this.taskRepository.getTaskAndAudioFile(
context,
audioFileId,
accountId,
[TASK_STATUS.UPLOADED],
);
if (!file) {
throw new Error('audio file not found');
}
// タグ対象に通知送信
await this.notificationhubService.notify(context, tags, {
authorId: file.author_id,
filename: file.file_name.replace('.zip', ''),
priority: file.priority === '00' ? 'Normal' : 'High',
uploadedAt: file.uploaded_at.toISOString(),
});
this.logger.log(
`[OUT] [${context.getTrackingId()}] ${this.sendNotify.name}`,
);
}
}