Merged PR 562: キャンセルAPI修正
## 概要 [Task2972: キャンセルAPI修正](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/2972) - キャンセル処理に自動ルーティングを追加 ## レビューポイント - 追加したテストケースは足りているか - 自動ルーティングを修正したが修正箇所は問題ないか(To : 福永さん) - 特にworktypeが空文字だった時の挙動を修正したので、そこが業務要件とあっているか - コメントがある場所 ## UIの変更 - Before/Afterのスクショなど - スクショ置き場 ## 動作確認状況 - ローカルで確認 ## 補足 - 相談、参考資料などがあれば
This commit is contained in:
parent
c9124f8661
commit
1e4a545bf8
@ -299,9 +299,7 @@ export class AccountsController {
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@UseGuards(RoleGuard.requireds({ delegation: true }))
|
||||
@Get('typists')
|
||||
async getTypists(@Req() req: Request): Promise<GetTypistsResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
|
||||
@ -204,7 +204,6 @@ export class FilesService {
|
||||
await this.tasksRepositoryService.autoRouting(
|
||||
task.audio_file_id,
|
||||
user.account_id,
|
||||
workType,
|
||||
user.author_id ?? undefined,
|
||||
);
|
||||
|
||||
|
||||
@ -28,6 +28,15 @@ import {
|
||||
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
|
||||
import { makeTestingModule } from '../../common/test/modules';
|
||||
import { createSortCriteria } from '../users/test/utility';
|
||||
import { createWorktype } from '../accounts/test/utility';
|
||||
import {
|
||||
createWorkflow,
|
||||
createWorkflowTypist,
|
||||
} from '../workflows/test/utility';
|
||||
import { createTemplateFile } from '../templates/test/utility';
|
||||
import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service';
|
||||
import { makeNotifyMessage } from '../../common/notify/makeNotifyMessage';
|
||||
import { Roles } from '../../common/types/role';
|
||||
|
||||
describe('TasksService', () => {
|
||||
it('タスク一覧を取得できる(admin)', async () => {
|
||||
@ -2468,6 +2477,253 @@ describe('cancel', () => {
|
||||
new HttpException(makeErrorResponse('E010603'), HttpStatus.NOT_FOUND),
|
||||
);
|
||||
});
|
||||
|
||||
it('API実行者のRoleがTypistの場合、自身が文字起こし実行中のタスクをキャンセルし、そのタスクの自動ルーティングを行う', async () => {
|
||||
if (!source) fail();
|
||||
const notificationhubServiceMockValue =
|
||||
makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTaskTestingModuleWithNotificaiton(
|
||||
source,
|
||||
notificationhubServiceMockValue,
|
||||
);
|
||||
if (!module) fail();
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: typistUserId } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: 'typist',
|
||||
});
|
||||
const { id: authorUserId, author_id } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'author-user-external-id',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID',
|
||||
});
|
||||
//ワークタイプIDを作成
|
||||
await createWorktype(source, accountId, '01');
|
||||
// テンプレートファイルを作成
|
||||
const { id: templateFileId } = await createTemplateFile(
|
||||
source,
|
||||
accountId,
|
||||
'template-file-name',
|
||||
'https://example.com',
|
||||
);
|
||||
// ワークフローを作成
|
||||
const { id: workflowId } = await createWorkflow(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
undefined,
|
||||
templateFileId,
|
||||
);
|
||||
// ワークフロータイピストを作成
|
||||
await createWorkflowTypist(source, workflowId, typistUserId);
|
||||
|
||||
const { taskId } = await createTask(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
author_id ?? '',
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
'InProgress',
|
||||
typistUserId,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const NotificationHubService = module.get<NotificationhubService>(
|
||||
NotificationhubService,
|
||||
);
|
||||
await service.cancel(
|
||||
makeContext('trackingId'),
|
||||
1,
|
||||
'typist-user-external-id',
|
||||
['typist', 'standard'],
|
||||
);
|
||||
const resultTask = await getTask(source, taskId);
|
||||
const permisions = await getCheckoutPermissions(source, taskId);
|
||||
|
||||
expect(resultTask?.status).toEqual('Uploaded');
|
||||
expect(resultTask?.typist_user_id).toEqual(null);
|
||||
// タスクのテンプレートファイルIDを確認
|
||||
expect(resultTask?.template_file_id).toEqual(templateFileId);
|
||||
// タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認
|
||||
expect(permisions.length).toEqual(1);
|
||||
expect(permisions[0].user_id).toEqual(typistUserId);
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(NotificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId'),
|
||||
[`user_${typistUserId}`],
|
||||
makeNotifyMessage('M000101'),
|
||||
);
|
||||
}, 1000000);
|
||||
|
||||
it('API実行者のRoleがAdminの場合、自身が文字起こし実行中のタスクをキャンセルし、そのタスクの自動ルーティングを行う(API実行者のAuthorIDと音声ファイルに紐づくWorkType)', async () => {
|
||||
if (!source) fail();
|
||||
const notificationhubServiceMockValue =
|
||||
makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTaskTestingModuleWithNotificaiton(
|
||||
source,
|
||||
notificationhubServiceMockValue,
|
||||
);
|
||||
if (!module) fail();
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
// タスクの文字起こし担当者
|
||||
const { id: typistUserId } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: 'typist',
|
||||
});
|
||||
// 自動ルーティングされるタイピストユーザーを作成
|
||||
const { id: autoRoutingTypistUserId } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'auto-routing-typist-user-external-id',
|
||||
role: 'typist',
|
||||
});
|
||||
// API実行者
|
||||
const {
|
||||
id: myAuthorUserId,
|
||||
external_id,
|
||||
role,
|
||||
} = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'my-author-user-external-id',
|
||||
role: 'author admin',
|
||||
author_id: 'MY_AUTHOR_ID',
|
||||
});
|
||||
// 音声ファイルのアップロード者
|
||||
const { id: authorUserId, author_id } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'author-user-external-id',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID',
|
||||
});
|
||||
//ワークタイプIDを作成
|
||||
const { id: workTypeId, custom_worktype_id } = await createWorktype(
|
||||
source,
|
||||
accountId,
|
||||
'01',
|
||||
);
|
||||
// テンプレートファイルを作成
|
||||
const { id: templateFileId } = await createTemplateFile(
|
||||
source,
|
||||
accountId,
|
||||
'template-file-name',
|
||||
'https://example.com',
|
||||
);
|
||||
// ワークフローを作成
|
||||
const { id: workflowId } = await createWorkflow(
|
||||
source,
|
||||
accountId,
|
||||
myAuthorUserId,
|
||||
workTypeId,
|
||||
templateFileId,
|
||||
);
|
||||
// ワークフロータイピストを作成
|
||||
await createWorkflowTypist(source, workflowId, autoRoutingTypistUserId);
|
||||
|
||||
const { taskId } = await createTask(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
author_id ?? '',
|
||||
custom_worktype_id,
|
||||
'01',
|
||||
'00000001',
|
||||
'InProgress',
|
||||
typistUserId,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const NotificationHubService = module.get<NotificationhubService>(
|
||||
NotificationhubService,
|
||||
);
|
||||
await service.cancel(
|
||||
makeContext('trackingId'),
|
||||
1,
|
||||
external_id,
|
||||
role.split(' ') as Roles[],
|
||||
);
|
||||
const resultTask = await getTask(source, taskId);
|
||||
const permisions = await getCheckoutPermissions(source, taskId);
|
||||
|
||||
expect(resultTask?.status).toEqual('Uploaded');
|
||||
expect(resultTask?.typist_user_id).toEqual(null);
|
||||
// タスクのテンプレートファイルIDを確認
|
||||
expect(resultTask?.template_file_id).toEqual(templateFileId);
|
||||
// タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認
|
||||
expect(permisions.length).toEqual(1);
|
||||
expect(permisions[0].user_id).toEqual(autoRoutingTypistUserId);
|
||||
// 通知処理が想定通りの引数で呼ばれているか確認
|
||||
expect(NotificationHubService.notify).toHaveBeenCalledWith(
|
||||
makeContext('trackingId'),
|
||||
[`user_${autoRoutingTypistUserId}`],
|
||||
makeNotifyMessage('M000101'),
|
||||
);
|
||||
});
|
||||
it('API実行者のRoleがTypistの場合、自身が文字起こし実行中のタスクをキャンセルするが、一致するワークフローがない場合は自動ルーティングを行うことができない', async () => {
|
||||
if (!source) fail();
|
||||
const notificationhubServiceMockValue =
|
||||
makeDefaultNotificationhubServiceMockValue();
|
||||
const module = await makeTaskTestingModuleWithNotificaiton(
|
||||
source,
|
||||
notificationhubServiceMockValue,
|
||||
);
|
||||
if (!module) fail();
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
// タスクの文字起こし担当者
|
||||
const {
|
||||
id: typistUserId,
|
||||
external_id,
|
||||
role,
|
||||
} = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: 'typist',
|
||||
});
|
||||
// 音声ファイルのアップロード者
|
||||
const { id: authorUserId, author_id } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'author-user-external-id',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID',
|
||||
});
|
||||
const { taskId } = await createTask(
|
||||
source,
|
||||
accountId,
|
||||
authorUserId,
|
||||
author_id ?? '',
|
||||
'custom_worktype_id',
|
||||
'01',
|
||||
'00000001',
|
||||
'InProgress',
|
||||
typistUserId,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const NotificationHubService = module.get<NotificationhubService>(
|
||||
NotificationhubService,
|
||||
);
|
||||
await service.cancel(
|
||||
makeContext('trackingId'),
|
||||
1,
|
||||
external_id,
|
||||
role.split(' ') as Roles[],
|
||||
);
|
||||
const resultTask = await getTask(source, taskId);
|
||||
const permisions = await getCheckoutPermissions(source, taskId);
|
||||
|
||||
expect(resultTask?.status).toEqual('Uploaded');
|
||||
expect(resultTask?.typist_user_id).toEqual(null);
|
||||
// タスクのチェックアウト権限が削除されていることを確認
|
||||
expect(permisions.length).toEqual(0);
|
||||
// 通知処理が想定通りの引数で呼ばれていないか確認
|
||||
expect(NotificationHubService.notify).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextTask', () => {
|
||||
|
||||
@ -33,6 +33,7 @@ import { NotificationhubService } from '../../gateways/notificationhub/notificat
|
||||
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
|
||||
import { makeNotifyMessage } from '../../common/notify/makeNotifyMessage';
|
||||
import { Context } from '../../common/log';
|
||||
import { User } from '../../repositories/users/entity/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TasksService {
|
||||
@ -382,19 +383,29 @@ export class TasksService {
|
||||
externalId: string,
|
||||
role: Roles[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.cancel.name} | params: { audioFileId: ${audioFileId}, externalId: ${externalId}, role: ${role} };`,
|
||||
);
|
||||
let user: User;
|
||||
try {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.cancel.name} | params: { audioFileId: ${audioFileId}, externalId: ${externalId}, role: ${role} };`,
|
||||
// ユーザー取得
|
||||
user = await this.usersRepository.findUserByExternalId(externalId);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancel.name}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
const { id, account_id } =
|
||||
await this.usersRepository.findUserByExternalId(externalId);
|
||||
}
|
||||
|
||||
try {
|
||||
// roleにAdminが含まれていれば、文字起こし担当でなくてもキャンセルできるため、ユーザーIDは指定しない
|
||||
return await this.taskRepository.cancel(
|
||||
await this.taskRepository.cancel(
|
||||
audioFileId,
|
||||
[TASK_STATUS.IN_PROGRESS, TASK_STATUS.PENDING],
|
||||
account_id,
|
||||
role.includes(ADMIN_ROLES.ADMIN) ? undefined : id,
|
||||
user.account_id,
|
||||
role.includes(ADMIN_ROLES.ADMIN) ? undefined : user.id,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
@ -422,6 +433,47 @@ export class TasksService {
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// キャンセルしたタスクに自動ルーティングを行う
|
||||
const { typistGroupIds, typistIds } =
|
||||
await this.taskRepository.autoRouting(
|
||||
audioFileId,
|
||||
user.account_id,
|
||||
user.author_id ?? undefined,
|
||||
);
|
||||
|
||||
const groupMembers =
|
||||
await this.userGroupsRepositoryService.getGroupMembersFromGroupIds(
|
||||
typistGroupIds,
|
||||
);
|
||||
|
||||
// 重複のない割り当て候補ユーザーID一覧を取得する
|
||||
const distinctUserIds = [
|
||||
...new Set([...typistIds, ...groupMembers.map((x) => x.user_id)]),
|
||||
];
|
||||
|
||||
// 割り当てられたユーザーがいない場合は通知不要
|
||||
if (distinctUserIds.length === 0) {
|
||||
this.logger.log('No user assigned.');
|
||||
return;
|
||||
}
|
||||
|
||||
// タグを生成
|
||||
const tags = distinctUserIds.map((x) => `user_${x}`);
|
||||
this.logger.log(`tags: ${tags}`);
|
||||
|
||||
// タグ対象に通知送信
|
||||
await this.notificationhubService.notify(
|
||||
context,
|
||||
tags,
|
||||
makeNotifyMessage('M000101'),
|
||||
);
|
||||
} catch (e) {
|
||||
// 処理の本筋はタスクキャンセルのため自動ルーティングに失敗してもエラーにしない
|
||||
this.logger.error(`Automatic routing or notification failed.`);
|
||||
this.logger.error(`error=${e}`);
|
||||
} finally {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.cancel.name}`);
|
||||
}
|
||||
|
||||
@ -957,17 +957,15 @@ export class TasksRepositoryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* worktypeIdをもとにルーティングルールを取得し、タスクのチェックアウト権限を設定する
|
||||
* ルーティングルールを取得し、タスクのチェックアウト権限を設定する
|
||||
* @param audioFileId
|
||||
* @param accountId
|
||||
* @param worktypeId
|
||||
* @param [myAuthorId]
|
||||
* @returns typistIds: タイピストIDの一覧 / typistGroupIds: タイピストグループIDの一覧
|
||||
*/
|
||||
async autoRouting(
|
||||
audioFileId: number,
|
||||
accountId: number,
|
||||
worktypeId: string, // ユーザーが任意につけるworktypeId(DBのcustom_worktype_id)
|
||||
myAuthorId?: string, // API実行者のAuthorId
|
||||
): Promise<{ typistIds: number[]; typistGroupIds: number[] }> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
@ -1008,17 +1006,20 @@ export class TasksRepositoryService {
|
||||
`user not found. authorId:${audioFile.author_id}, accountId:${accountId}`,
|
||||
);
|
||||
}
|
||||
// ユーザーが任意につけるworktypeIdをもとにworktypeを取得
|
||||
|
||||
// 音声ファイル上のworktypeIdをもとにworktypeを取得
|
||||
const worktypeRepo = entityManager.getRepository(Worktype);
|
||||
const worktypeRecord = await worktypeRepo.findOne({
|
||||
where: {
|
||||
custom_worktype_id: worktypeId,
|
||||
custom_worktype_id: audioFile.work_type_id,
|
||||
account_id: accountId,
|
||||
},
|
||||
});
|
||||
if (!worktypeRecord) {
|
||||
|
||||
// 音声ファイル上のworktypeIdが設定されているが、一致するworktypeが存在しない場合はエラーを出して終了
|
||||
if (!worktypeRecord && audioFile.work_type_id !== '') {
|
||||
throw new Error(
|
||||
`worktype not found. worktype:${worktypeId}, accountId:${accountId}`,
|
||||
`worktype not found. worktype:${audioFile.work_type_id}, accountId:${accountId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1031,7 +1032,7 @@ export class TasksRepositoryService {
|
||||
where: {
|
||||
account_id: accountId,
|
||||
author_id: authorUser.id,
|
||||
worktype_id: worktypeRecord.id,
|
||||
worktype_id: worktypeRecord?.id ?? IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
@ -1071,14 +1072,14 @@ export class TasksRepositoryService {
|
||||
where: {
|
||||
account_id: accountId,
|
||||
author_id: myAuthorUser.id,
|
||||
worktype_id: worktypeRecord.id,
|
||||
worktype_id: worktypeRecord?.id ?? IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
// API実行者のAuthorIdと音声ファイルのWorktypeをもとにルーティングルールを取得できない場合はエラーを出して終了
|
||||
if (!defaultWorkflow) {
|
||||
throw new Error(
|
||||
`workflow not found. authorUserId:${myAuthorUser.id}, accountId:${accountId}, worktype:${worktypeId}`,
|
||||
`workflow not found. authorUserId:${myAuthorUser.id}, accountId:${accountId}, worktypeId:${worktypeRecord?.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user