saito.k e4ba5229df Merged PR 224: タスク中断API実装
## 概要
[Task2119: タスク中断API実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2119)

- タスク中断APIの処理を実装
- テスト実装
- チェックアウト処理のエラーチェックを修正
  - エラーが発生したときに何が原因なのかログに出力するように修正

## レビューポイント
- チェックインと同様の処理部分を切り出さずにそのまま実装したが、スマートに切り出せる方法はありそうか。
  - チェックインとサスペンドの取得処理だけ切り出してもあまりうれしくない(この2つ以外のところで使えなさそう)
    - キャンセルでは使えるかもだけど
  - チェックアウトやその他のメソッドのタスク取得とまとめようとすると、チェック内容や検索条件に差異がありきれいに切り出すことができなさそう。
    - まとめようとすると、引数が膨大でチェック項目も複雑になる

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

## 動作確認状況
- ローカルで確認

## 補足
- コンフリクトが発生しているが、現在のdevelopを取り込むとエラーになるので解消し次第、取り込んで競合解決します
- チェックインAPIの実装も入っていますが、そこは別のPRでレビューを行っているので対象外となります
2023-07-12 08:27:46 +00:00

381 lines
11 KiB
TypeScript

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<void> {
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<void> {
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<void> {
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<AdB2cUser[]> {
// 割り当て候補の外部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<void> {
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,
);
}
}
}