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'; @Injectable() export class TasksService { private readonly logger = new Logger(TasksService.name); constructor( private readonly taskRepository: TasksRepositoryService, private readonly usersRepository: UsersRepositoryService, private readonly adB2cService: AdB2cService, ) {} // TODO: 引数にAccessTokenがあるのは不適切なのでController側で分解したい async getTasks( accessToken: AccessToken, offset: number, limit: number, status: string[], paramName?: TaskListSortableAttribute, direction?: SortDirection, ): Promise<{ tasks: Task[]; total: number }> { const { role, userId } = accessToken; const roles = role.split(' '); // TODO: Roleを型で定義されているものに修正する // パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える const defaultParamName: TaskListSortableAttribute = 'JOB_NUMBER'; const defaultDirection: SortDirection = 'ASC'; 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, ); // B2Cからユーザー名を取得する const b2cUsers = await this.getB2cUsers( 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, ); // B2Cからユーザー名を取得する const b2cUsers = await this.getB2cUsers( 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, ); // B2Cからユーザー名を取得する const b2cUsers = await this.getB2cUsers( 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, ); } } /** * 指定した音声ファイルに紐づくタスクをcheckoutする * @param audioFileId * @param roles * @param externalId * @returns checkout */ async checkout( audioFileId: number, roles: Roles[], externalId: string, ): Promise { try { 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: 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, ); } } /** * 指定した音声ファイルに紐づくタスクをcheckinする * @param audioFileId * @param externalId * @returns checkin */ async checkin(audioFileId: number, externalId: string): Promise { try { 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.BAD_REQUEST, ); 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, ); } } /** * 指定した音声ファイルに紐づくタスクをsuspendする * @param audioFileId * @param externalId * @returns suspend */ async suspend(audioFileId: number, externalId: string): Promise { try { 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.BAD_REQUEST, ); 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, ); } } private async getB2cUsers( 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(filteredExternalIds); } /** * Changes checkout permission * @param audioFileId * @param assignees * @returns checkout permission */ async changeCheckoutPermission( audioFileId: number, assignees: Assignee[], externalId: string, role: Roles[], ): Promise { try { const { author_id, account_id } = await this.usersRepository.findUserByExternalId(externalId); await this.taskRepository.changeCheckoutPermission( audioFileId, author_id, account_id, role, assignees, ); } 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, ); } } }