Merged PR 231: タスクキャンセルAPI実装

## 概要
[Task2120: タスクキャンセルAPI実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2120)

- タスクキャンセルAPIを実装
- テスト実装

## レビューポイント
- Adminの時とTypistの時の実行条件はあっているか
- テストケースは足りているか

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

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

## 補足
- 中断、チェックインAPIの実装が入っていますが、それに関しては別PRでレビューしていただいているので対象外とさせてください
This commit is contained in:
saito.k 2023-07-13 06:55:12 +00:00
parent b4026c1460
commit 14627ad7e9
4 changed files with 419 additions and 10 deletions

View File

@ -242,7 +242,7 @@ export class TasksController {
json: true,
}) as AccessToken;
this.taskService.checkin(audioFileId, userId);
await this.taskService.checkin(audioFileId, userId);
return {};
}
@ -277,14 +277,26 @@ export class TasksController {
description:
'指定した文字起こしタスクをキャンセルしますステータスをUploadedにします',
})
@UseGuards(AuthGuard)
@UseGuards(
RoleGuard.requireds({
roles: [ADMIN_ROLES.ADMIN, USER_ROLES.TYPIST],
}),
)
@ApiBearerAuth()
async cancel(
@Headers() headers,
@Req() req: Request,
@Param() params: ChangeStatusRequest,
): Promise<ChangeStatusResponse> {
const { audioFileId } = params;
console.log(audioFileId);
// AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない
const accessToken = retrieveAuthorizationToken(req);
const { userId, role } = jwt.decode(accessToken, {
json: true,
}) as AccessToken;
// RoleGuardでroleの文字列に想定外の文字列や重複がないことは担保されているためここでは型変換のみ行う
const roles = role.split(' ') as Roles[];
await this.taskService.cancel(audioFileId, userId, roles);
return {};
}
@ -337,7 +349,7 @@ export class TasksController {
json: true,
}) as AccessToken;
this.taskService.suspend(audioFileId, userId);
await this.taskService.suspend(audioFileId, userId);
return {};
}

View File

@ -1714,7 +1714,7 @@ describe('suspend', () => {
'01',
'00000001',
'InProgress',
// API実行者のタスクではないため、typist_user_idは設定しない
anotherTypistUserId,
);
await createCheckoutPermissions(source, taskId, anotherTypistUserId);
@ -1745,3 +1745,282 @@ describe('suspend', () => {
);
});
});
describe('cancel', () => {
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実行者のRoleがTypistの場合、自身が文字起こし実行中のタスクをキャンセルできる', 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);
await service.cancel(1, 'typist-user-external-id', ['typist', 'standard']);
const { status, typist_user_id } = await getTask(source, taskId);
const permisions = await getCheckoutPermissions(source, taskId);
expect(status).toEqual('Uploaded');
expect(typist_user_id).toEqual(null);
expect(permisions.length).toEqual(0);
});
it('API実行者のRoleがTypistの場合、自身が文字起こし中断しているタスクをキャンセルできる', 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',
'Pending',
typistUserId,
);
await createCheckoutPermissions(source, taskId, typistUserId);
const service = module.get<TasksService>(TasksService);
await service.cancel(1, 'typist-user-external-id', ['typist', 'standard']);
const { status, typist_user_id } = await getTask(source, taskId);
const permisions = await getCheckoutPermissions(source, taskId);
expect(status).toEqual('Uploaded');
expect(typist_user_id).toEqual(null);
expect(permisions.length).toEqual(0);
});
it('API実行者のRoleがAdminの場合、文字起こし実行中のタスクをキャンセルできる', 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);
await service.cancel(1, 'typist-user-external-id', ['admin', 'author']);
const { status, typist_user_id } = await getTask(source, taskId);
const permisions = await getCheckoutPermissions(source, taskId);
expect(status).toEqual('Uploaded');
expect(typist_user_id).toEqual(null);
expect(permisions.length).toEqual(0);
});
it('API実行者のRoleがAdminの場合、文字起こし中断しているタスクをキャンセルできる', 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',
'Pending',
typistUserId,
);
await createCheckoutPermissions(source, taskId, typistUserId);
const service = module.get<TasksService>(TasksService);
await service.cancel(1, 'typist-user-external-id', ['admin', 'author']);
const { status, typist_user_id } = await getTask(source, taskId);
const permisions = await getCheckoutPermissions(source, taskId);
expect(status).toEqual('Uploaded');
expect(typist_user_id).toEqual(null);
expect(permisions.length).toEqual(0);
});
it('タスクのステータスが[Inprogress,Pending]でない時、タスクをキャンセルできない', 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.cancel(1, 'typist-user-external-id', ['admin', 'author']),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010601'), HttpStatus.BAD_REQUEST),
);
});
it('API実行者のRoleがTypistの場合、他人が文字起こし実行中のタスクをキャンセルできない', 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',
anotherTypistUserId,
);
await createCheckoutPermissions(source, taskId, anotherTypistUserId);
const service = module.get<TasksService>(TasksService);
await expect(
service.cancel(1, 'typist-user-external-id', ['typist', 'standard']),
).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.cancel(1, 'typist-user-external-id', ['typist', 'standard']),
).rejects.toEqual(
new HttpException(makeErrorResponse('E010603'), HttpStatus.BAD_REQUEST),
);
});
});

View File

@ -249,6 +249,57 @@ export class TasksService {
);
}
}
/**
*
* @param audioFileId
* @param externalId
* @param role
* @returns cancel
*/
async cancel(
audioFileId: number,
externalId: string,
role: Roles[],
): Promise<void> {
try {
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.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する
@ -327,7 +378,7 @@ export class TasksService {
return await this.adB2cService.getUsers(filteredExternalIds);
}
/**
* Changes checkout permission
*
* @param audioFileId
* @param assignees
* @returns checkout permission

View File

@ -168,7 +168,7 @@ export class TasksRepositoryService {
// ステータスチェック
if (!permittedSourceStatus.includes(task.status)) {
throw new StatusNotMatchError(
`Unexpected task status. status:${task.status}`,
`Unexpected task status. audio_file_id:${audio_file_id}, status:${task.status}`,
);
}
@ -266,12 +266,12 @@ export class TasksRepositoryService {
}
if (task.status !== permittedSourceStatus) {
throw new StatusNotMatchError(
`Unexpected task status. status:${task.status}`,
`Unexpected task status. audio_file_id:${audio_file_id}, 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}`,
`TypistUser not match. audio_file_id:${audio_file_id}, typist_user_id:${task.typist_user_id}, user_id:${user_id}`,
);
}
@ -286,6 +286,73 @@ export class TasksRepositoryService {
});
}
/**
* IDで指定したタスクをキャンセルする
* @param audio_file_id ID
* @param permittedSourceStatus
* @param account_id ID
* @param user_id ID(API実行者がAdminのときは使用しない)
* @returns cancel
*/
async cancel(
audio_file_id: number,
permittedSourceStatus: TaskStatus[],
account_id: number,
user_id?: number | undefined,
): Promise<void> {
await this.dataSource.transaction(async (entityManager) => {
const taskRepo = entityManager.getRepository(Task);
const task = await taskRepo.findOne({
where: {
audio_file_id: audio_file_id,
},
});
if (!task) {
throw new TasksNotFoundError(
`task not found. audio_file_id:${audio_file_id}`,
);
}
if (!isTaskStatus(task.status)) {
throw new Error('invalid task status');
}
// ステータスチェック
if (!permittedSourceStatus.includes(task.status)) {
throw new StatusNotMatchError(
`Unexpected task status. audio_file_id:${audio_file_id}, status:${task.status}`,
);
}
if (task.account_id !== account_id) {
throw new AccountNotMatchError(
`task account_id not match. audio_file_id:${audio_file_id}, account_id(Task):${task.account_id}, account_id:${account_id}`,
);
}
if (user_id && task.typist_user_id !== user_id) {
throw new TypistUserNotMatchError(
`TypistUser not match. audio_file_id:${audio_file_id}, typist_user_id:${task.typist_user_id}, user_id:${user_id}`,
);
}
// 対象タスクの文字起こし担当をnull,ステータスをUploadedに更新
await taskRepo.update(
{ audio_file_id: audio_file_id },
{
typist_user: null,
status: TASK_STATUS.UPLOADED,
},
);
const checkoutPermissionRepo =
entityManager.getRepository(CheckoutPermission);
// 対象タスクの文字起こし候補を削除
/* Inprogress,PendingID
()*/
await checkoutPermissionRepo.delete({
task_id: task.id,
});
});
}
/**
* IDで指定したタスクをsuspendする
* @param audio_file_id ID