import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; import { AccessToken } from '../../common/token'; import { Assignee, 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, TASK_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, 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 { makeNotifyMessage } from '../../common/notify/makeNotifyMessage'; import { Context } from '../../common/log'; @Injectable() export class TasksService { private readonly logger = new Logger(TasksService.name); constructor( private readonly taskRepository: TasksRepositoryService, private readonly usersRepository: UsersRepositoryService, private readonly userGroupsRepositoryService: UserGroupsRepositoryService, private readonly adB2cService: AdB2cService, private readonly notificationhubService: NotificationhubService, ) {} // TODO [Task2244] 引数にAccessTokenがあるのは不適切なのでController側で分解したい async getTasks( context: Context, accessToken: AccessToken, offset: number, limit: number, status?: string[], paramName?: TaskListSortableAttribute, direction?: SortDirection, ): Promise<{ tasks: Task[]; total: number }> { this.logger.log( `[IN] [${context.trackingId}] ${this.getTasks.name} | params: { offset: ${offset}, limit: ${limit}, status: ${status}, paramName: ${paramName}, direction: ${direction} };`, ); const { role, userId } = accessToken; // TODO [Task2244] Roleに型で定義されている値が入っているかをチェックして異常値を弾く実装に修正する const roles = role.split(' '); // パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える 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(userId); if (roles.includes(ADMIN_ROLES.ADMIN)) { const result = await this.taskRepository.getTasksFromAccountId( 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)) { const result = await this.taskRepository.getTasksFromAuthorIdAndAccountId( 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( 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(`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.trackingId}] ${this.getTasks.name}`); } } /** * 指定した音声ファイルに紐づくタスクをcheckoutする * @param audioFileId * @param roles * @param externalId * @returns checkout */ async checkout( context: Context, audioFileId: number, roles: Roles[], externalId: string, ): Promise { try { this.logger.log( `[IN] [${context.trackingId}] ${this.checkout.name} | params: { audioFileId: ${audioFileId}, roles: ${roles}, externalId: ${externalId} };`, ); const { id, account_id, author_id } = await this.usersRepository.findUserByExternalId(externalId); if (roles.includes(USER_ROLES.AUTHOR)) { await this.taskRepository.getTaskFromAudioFileId( audioFileId, account_id, author_id, ); return; } if (roles.includes(USER_ROLES.TYPIST)) { return await this.taskRepository.checkout(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(`error=${e}`); if (e instanceof Error) { switch (e.constructor) { 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: 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.trackingId}] ${this.checkout.name}`); } } /** * 指定した音声ファイルに紐づくタスクをcheckinする * @param audioFileId * @param externalId * @returns checkin */ async checkin( context: Context, audioFileId: number, externalId: string, ): Promise { try { this.logger.log( `[IN] [${context.trackingId}] ${this.checkin.name} | params: { audioFileId: ${audioFileId}, externalId: ${externalId} };`, ); const { id } = await this.usersRepository.findUserByExternalId( externalId, ); return await this.taskRepository.checkin( audioFileId, id, TASK_STATUS.IN_PROGRESS, ); } catch (e) { this.logger.error(`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.trackingId}] ${this.checkin.name}`); } } /** * 指定した音声ファイルに紐づくタスクをキャンセルする * @param audioFileId * @param externalId * @param role * @returns cancel */ async cancel( context: Context, audioFileId: number, externalId: string, role: Roles[], ): Promise { try { this.logger.log( `[IN] [${context.trackingId}] ${this.cancel.name} | params: { audioFileId: ${audioFileId}, externalId: ${externalId}, role: ${role} };`, ); const { id, account_id } = await this.usersRepository.findUserByExternalId(externalId); // roleにAdminが含まれていれば、文字起こし担当でなくてもキャンセルできるため、ユーザーIDは指定しない return await this.taskRepository.cancel( audioFileId, [TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING], account_id, role.includes(ADMIN_ROLES.ADMIN) ? undefined : id, ); } catch (e) { this.logger.error(`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.trackingId}] ${this.cancel.name}`); } } /** * 指定した音声ファイルに紐づくタスクをsuspendする * @param audioFileId * @param externalId * @returns suspend */ async suspend( context: Context, audioFileId: number, externalId: string, ): Promise { try { this.logger.log( `[IN] [${context.trackingId}] ${this.suspend.name} | params: { audioFileId: ${audioFileId}, externalId: ${externalId} };`, ); const { id } = await this.usersRepository.findUserByExternalId( externalId, ); return await this.taskRepository.suspend( audioFileId, id, TASK_STATUS.IN_PROGRESS, ); } catch (e) { this.logger.error(`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.trackingId}] ${this.suspend.name}`); } } private async getB2cUsers( context: Context, tasks: TaskEntity[], permissions: CheckoutPermission[], ): Promise { // 割り当て候補の外部IDを列挙 const assigneesExternalIds = permissions.map((x) => { if (x.user) { return x.user.external_id; } }); // 割り当てられているタイピストの外部IDを列挙 const typistExternalIds = tasks.flatMap((x) => { if (x.typist_user) { return x.typist_user.external_id; } }); //重複をなくす const distinctedExternalIds = [ ...new Set(assigneesExternalIds.concat(typistExternalIds)), ]; // undefinedがあった場合、取り除く const filteredExternalIds: string[] = distinctedExternalIds.filter( (x): x is string => x !== undefined, ); // B2Cからユーザー名を取得する return await this.adB2cService.getUsers(context, filteredExternalIds); } /** * 文字起こし候補を変更する * @param audioFileId * @param assignees * @returns checkout permission */ async changeCheckoutPermission( context: Context, audioFileId: number, assignees: Assignee[], externalId: string, role: Roles[], ): Promise { try { this.logger.log( `[IN] [${context.trackingId}] ${this.changeCheckoutPermission.name} | params: { audioFileId: ${audioFileId}, assignees: ${assignees}, externalId: ${externalId}, role: ${role} };`, ); const { author_id, account_id } = await this.usersRepository.findUserByExternalId(externalId); await this.taskRepository.changeCheckoutPermission( audioFileId, author_id, account_id, role, assignees, ); // すべての割り当て候補ユーザーを取得する const assigneesGroupIds = assignees .filter((x) => x.typistGroupId) .map((x) => x.typistGroupId); const assigneesUserIds = assignees .filter((x) => x.typistUserId) .map((x) => x.typistUserId); const groupMembers = await this.userGroupsRepositoryService.getGroupMembersFromGroupIds( assigneesGroupIds, ); // 重複のない割り当て候補ユーザーID一覧を取得する const distinctUserIds = [ ...new Set([ ...assigneesUserIds, ...groupMembers.map((x) => x.user_id), ]), ]; // 割り当てられたユーザーがいない場合は通知不要 if (distinctUserIds.length === 0) { this.logger.log('No user assigned.'); return; } // タグを生成 const tags = distinctUserIds.map((x) => `user_${x}`); this.logger.log(`tags: ${tags}`); // タグ対象に通知送信 await this.notificationhubService.notify( context, tags, makeNotifyMessage('M000101'), ); } catch (e) { this.logger.error(`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.trackingId}] ${this.changeCheckoutPermission.name}`, ); } } }