Merged PR 219: タスクチェックインAPI実装
## 概要 [Task2118: タスクチェックインAPI実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2118) - チェックインAPIの処理を実装 - テスト実装 ## レビューポイント - 文字起こし担当であるかどうかをチェックする方法についてどちらが良いか - チェックアウト権限テーブルで、タスクに紐づく割り当て候補を確認する(チェックアウトした時点で個人指定のみとなっているはず) - タスク情報にあるtypist_user_idで確認する - テストケースは足りているか ## UIの変更 ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば
This commit is contained in:
parent
9c0f457e9f
commit
7be4da29bb
6
dictation_server/src/common/types/taskStatus/index.ts
Normal file
6
dictation_server/src/common/types/taskStatus/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { TASK_STATUS } from '../../../constants';
|
||||
|
||||
/**
|
||||
* Token.roleに配置されうる文字列リテラル型
|
||||
*/
|
||||
export type TaskStatus = (typeof TASK_STATUS)[keyof typeof TASK_STATUS];
|
||||
@ -12,13 +12,13 @@ import {
|
||||
} from '../../constants/index';
|
||||
import { User } from '../../repositories/users/entity/user.entity';
|
||||
import {
|
||||
AccountNotMatchError,
|
||||
AudioFileNotFoundError,
|
||||
AuthorUserNotMatchError,
|
||||
StatusNotMatchError,
|
||||
TemplateFileNotFoundError,
|
||||
} from './errors/types';
|
||||
import {
|
||||
AccountNotMatchError,
|
||||
StatusNotMatchError,
|
||||
TasksNotFoundError,
|
||||
TypistUserNotFoundError,
|
||||
} from '../../repositories/tasks/errors/types';
|
||||
|
||||
@ -170,6 +170,12 @@ export class TasksController {
|
||||
'指定した文字起こしタスクをチェックアウトします(ステータスをInprogressにします)',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({
|
||||
roles: [USER_ROLES.AUTHOR, USER_ROLES.TYPIST],
|
||||
}),
|
||||
)
|
||||
async checkout(
|
||||
@Req() req: Request,
|
||||
@Param() param: ChangeStatusRequest,
|
||||
@ -219,13 +225,24 @@ export class TasksController {
|
||||
'指定した文字起こしタスクをチェックインします(ステータスをFinishedにします)',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({
|
||||
roles: [USER_ROLES.TYPIST],
|
||||
}),
|
||||
)
|
||||
async checkin(
|
||||
@Headers() headers,
|
||||
@Req() req: Request,
|
||||
@Param() params: ChangeStatusRequest,
|
||||
): Promise<ChangeStatusResponse> {
|
||||
const { audioFileId } = params;
|
||||
console.log(audioFileId);
|
||||
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
const { userId } = jwt.decode(accessToken, {
|
||||
json: true,
|
||||
}) as AccessToken;
|
||||
|
||||
this.taskService.checkin(audioFileId, userId);
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@ -1447,3 +1447,154 @@ describe('checkout', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkin', () => {
|
||||
let source: DataSource = null;
|
||||
beforeEach(async () => {
|
||||
source = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
logging: false,
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
|
||||
});
|
||||
return source.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await source.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('API実行者が文字起こし実行中のタスクである場合、タスクをチェックインできる', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
const { accountId } = await createAccount(source);
|
||||
const { userId: typistUserId } = await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'typist-user-external-id',
|
||||
'typist',
|
||||
);
|
||||
const { userId: authorUserId } = await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'author-user-external-id',
|
||||
'author',
|
||||
'MY_AUTHOR_ID',
|
||||
);
|
||||
const { taskId } = await createTask(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
'InProgress',
|
||||
typistUserId,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
|
||||
const initTask = await getTask(source, taskId);
|
||||
|
||||
await service.checkin(1, 'typist-user-external-id');
|
||||
const { status, finished_at } = await getTask(source, taskId);
|
||||
|
||||
expect(status).toEqual('Finished');
|
||||
expect(finished_at).not.toEqual(initTask.finished_at);
|
||||
});
|
||||
|
||||
it('タスクのステータスがInprogressでない時、タスクをチェックインできない', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
const { accountId } = await createAccount(source);
|
||||
const { userId: typistUserId } = await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'typist-user-external-id',
|
||||
'typist',
|
||||
);
|
||||
const { userId: authorUserId } = await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'author-user-external-id',
|
||||
'author',
|
||||
'MY_AUTHOR_ID',
|
||||
);
|
||||
const { taskId } = await createTask(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
'Uploaded',
|
||||
typistUserId,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
await expect(service.checkin(1, 'typist-user-external-id')).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E010601'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
|
||||
it('API実行者が文字起こし実行中のタスクでない場合、タスクをチェックインできない', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
const { accountId } = await createAccount(source);
|
||||
await createUser(source, accountId, 'typist-user-external-id', 'typist');
|
||||
const { userId: anotherTypistUserId } = await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'another-typist-user-external-id',
|
||||
'typist',
|
||||
);
|
||||
const { userId: authorUserId } = await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'author-user-external-id',
|
||||
'author',
|
||||
'MY_AUTHOR_ID',
|
||||
);
|
||||
const { taskId } = await createTask(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
'InProgress',
|
||||
// API実行者のタスクではないため、typist_user_idは設定しない
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, anotherTypistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
|
||||
await expect(service.checkin(1, 'typist-user-external-id')).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E010601'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
|
||||
it('タスクがない時、タスクをチェックインできない', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
const { accountId } = await createAccount(source);
|
||||
await createUser(source, accountId, 'typist-user-external-id', 'typist');
|
||||
|
||||
await createUser(
|
||||
source,
|
||||
accountId,
|
||||
'author-user-external-id',
|
||||
'author',
|
||||
'MY_AUTHOR_ID',
|
||||
);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
|
||||
await expect(service.checkin(1, 'typist-user-external-id')).rejects.toEqual(
|
||||
new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
SortDirection,
|
||||
TaskListSortableAttribute,
|
||||
} from '../../common/types/sort';
|
||||
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
|
||||
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
|
||||
import {
|
||||
AdB2cService,
|
||||
Adb2cTooManyRequestsError,
|
||||
@ -19,10 +19,12 @@ import { AdB2cUser } from '../../gateways/adb2c/types/types';
|
||||
import { CheckoutPermission } from '../../repositories/checkout_permissions/entity/checkout_permission.entity';
|
||||
import {
|
||||
CheckoutPermissionNotFoundError,
|
||||
StatusNotMatchError,
|
||||
TaskAuthorIdNotMatchError,
|
||||
TasksNotFoundError,
|
||||
TypistUserGroupNotFoundError,
|
||||
TypistUserNotFoundError,
|
||||
TypistUserNotMatchError,
|
||||
} from '../../repositories/tasks/errors/types';
|
||||
import { Roles } from '../../common/types/role';
|
||||
import { InvalidRoleError } from './errors/types';
|
||||
@ -195,6 +197,52 @@ export class TasksService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定した音声ファイルに紐づくタスクを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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getB2cUsers(
|
||||
tasks: TaskEntity[],
|
||||
permissions: CheckoutPermission[],
|
||||
|
||||
@ -62,6 +62,7 @@ export const createTask = async (
|
||||
priority: string,
|
||||
jobNumber: string,
|
||||
status: string,
|
||||
typist_user_id?: number | undefined,
|
||||
): Promise<{ taskId: number }> => {
|
||||
const { identifiers: audioFileIdentifiers } = await datasource
|
||||
.getRepository(AudioFile)
|
||||
@ -90,6 +91,7 @@ export const createTask = async (
|
||||
is_job_number_enabled: true,
|
||||
audio_file_id: audioFile.id,
|
||||
status: status,
|
||||
typist_user_id: typist_user_id,
|
||||
priority: priority,
|
||||
started_at: new Date().toISOString(),
|
||||
created_at: new Date(),
|
||||
|
||||
@ -8,3 +8,9 @@ export class TasksNotFoundError extends Error {}
|
||||
export class TaskAuthorIdNotMatchError extends Error {}
|
||||
// チェックアウト権限未発見エラー
|
||||
export class CheckoutPermissionNotFoundError extends Error {}
|
||||
// Status不一致エラー
|
||||
export class StatusNotMatchError extends Error {}
|
||||
// TypistUser不一致エラー
|
||||
export class TypistUserNotMatchError extends Error {}
|
||||
// Account不一致エラー
|
||||
export class AccountNotMatchError extends Error {}
|
||||
|
||||
@ -21,17 +21,17 @@ import { Assignee } from '../../features/tasks/types/types';
|
||||
import { UserGroup } from '../user_groups/entity/user_group.entity';
|
||||
import { User } from '../users/entity/user.entity';
|
||||
import {
|
||||
AccountNotMatchError,
|
||||
CheckoutPermissionNotFoundError,
|
||||
StatusNotMatchError,
|
||||
TaskAuthorIdNotMatchError,
|
||||
TasksNotFoundError,
|
||||
TypistUserGroupNotFoundError,
|
||||
TypistUserNotFoundError,
|
||||
TypistUserNotMatchError,
|
||||
} from './errors/types';
|
||||
import { Roles } from '../../common/types/role';
|
||||
import {
|
||||
AccountNotMatchError,
|
||||
StatusNotMatchError,
|
||||
} from '../../features/files/errors/types';
|
||||
import { TaskStatus } from '../../common/types/taskStatus';
|
||||
|
||||
@Injectable()
|
||||
export class TasksRepositoryService {
|
||||
@ -124,7 +124,13 @@ export class TasksRepositoryService {
|
||||
return task;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 音声ファイルIDに紐づいたTaskをCheckoutする
|
||||
* @param audioFileId
|
||||
* @param account_id
|
||||
* @param user_id
|
||||
* @returns checkout
|
||||
*/
|
||||
async checkout(
|
||||
audioFileId: number,
|
||||
account_id: number,
|
||||
@ -134,11 +140,6 @@ export class TasksRepositoryService {
|
||||
const taskRepo = entityManager.getRepository(Task);
|
||||
// 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得
|
||||
const task = await taskRepo.findOne({
|
||||
relations: {
|
||||
file: true,
|
||||
option_items: true,
|
||||
typist_user: true,
|
||||
},
|
||||
where: {
|
||||
audio_file_id: audioFileId,
|
||||
account_id: account_id,
|
||||
@ -173,9 +174,6 @@ export class TasksRepositoryService {
|
||||
const checkoutRepo = entityManager.getRepository(CheckoutPermission);
|
||||
// 対象タスクに紐づくユーザーが含まれるチェックアウト権限を取得する
|
||||
const related = await checkoutRepo.find({
|
||||
relations: {
|
||||
task: true,
|
||||
},
|
||||
where: [
|
||||
{
|
||||
task_id: task.id,
|
||||
@ -226,6 +224,52 @@ export class TasksRepositoryService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Params tasks repository service
|
||||
* @param audioFileId チェックインするタスクの音声ファイルID
|
||||
* @param user_id チェックインするユーザーのID
|
||||
* @param permittedSourceStatus チェックイン可能なステータス
|
||||
* @returns checkin
|
||||
*/
|
||||
async checkin(
|
||||
audioFileId: number,
|
||||
user_id: number,
|
||||
permittedSourceStatus: TaskStatus,
|
||||
): Promise<void> {
|
||||
await this.dataSource.transaction(async (entityManager) => {
|
||||
const taskRepo = entityManager.getRepository(Task);
|
||||
const task = await taskRepo.findOne({
|
||||
where: {
|
||||
audio_file_id: audioFileId,
|
||||
},
|
||||
});
|
||||
if (!task) {
|
||||
throw new TasksNotFoundError(
|
||||
`task not found. audio_file_id:${audioFileId}`,
|
||||
);
|
||||
}
|
||||
if (task.status !== permittedSourceStatus) {
|
||||
throw new StatusNotMatchError(
|
||||
`Unexpected task status. status:${task.status}`,
|
||||
);
|
||||
}
|
||||
if (task.typist_user_id !== user_id) {
|
||||
throw new TypistUserNotMatchError(
|
||||
`TypistUser not match. typist_user_id:${task.typist_user_id}, user_id:${user_id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 対象タスクの文字起こし終了日時を現在時刻に更新。ステータスをFinishedに更新
|
||||
await taskRepo.update(
|
||||
{ audio_file_id: audioFileId },
|
||||
{
|
||||
finished_at: new Date().toISOString(),
|
||||
status: TASK_STATUS.FINISHED,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定したアカウントIDに紐づくTask関連情報の一覧を取得します
|
||||
* @param account_id
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user