saito.k 1189e676b9 Merged PR 178: API実装(タスクチェックアウトAPI (Typist))
## 概要
[Task1996: API実装(タスクチェックアウトAPI (Typist))](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/1996)

- タスクチェックアウトAPIのTypist用の処理を実装
- テスト実装

## レビューポイント
- DBアクセス処理に不足はないか
- テスト内容に不足はないか
- テストのチェック方法に問題はないか
  - 特に今回の「started_at」はcheckoutした日時を入れるが、それがいつなのかを完全一致でチェックするのは大変なため、checkout前とcheckout後で値が異なっていることを確認するまでのチェックとした
- changeCheckoutPermissionsのPathパラメータにつけたコメントについて
## UIの変更
- Before/Afterのスクショなど
- スクショ置き場

## 動作確認状況
- ローカルで確認
- テストが通ることを確認

## 補足
- 「タスク 1476: [Sp12-1]アクセストークンの寿命を2時間にする」も実施しています
2023-07-03 01:09:06 +00:00

272 lines
8.4 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, 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 {
CheckoutPermissionNotFoundError,
TasksNotFoundError,
TypistUserGroupNotFoundError,
TypistUserNotFoundError,
} 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 } =
await this.usersRepository.findUserByExternalId(externalId);
// TODO authorの処理は別タスクで対応
if (roles.includes(USER_ROLES.AUTHOR)) {
}
if (roles.includes(USER_ROLES.TYPIST)) {
return await this.taskRepository.checkout(audioFileId, account_id, id);
}
throw new InvalidRoleError(`invalid roles: ${roles.join(',')}`);
} catch (e) {
this.logger.error(`error=${e}`);
if (e instanceof Error) {
switch (e.constructor) {
case CheckoutPermissionNotFoundError:
case InvalidRoleError:
throw new HttpException(
makeErrorResponse('E010602'),
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,
);
}
}
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,
);
}
}
}