forked from Yara724/api
YARA-1259
This commit is contained in:
@@ -2558,11 +2558,21 @@ export class ExpertClaimService {
|
|||||||
if (!blame) {
|
if (!blame) {
|
||||||
throw new NotFoundException("Linked blame file not found.");
|
throw new NotFoundException("Linked blame file not found.");
|
||||||
}
|
}
|
||||||
if (!(blame as any).isMadeByFileMaker) {
|
const clientKey = requireActorClientKey(actor);
|
||||||
|
if (!blameCaseTouchesClient(blame, clientKey)) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"This file does not belong to your organization.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!(blame as any).isMadeByFileMaker ||
|
||||||
|
!(blame as any).expertInitiated ||
|
||||||
|
(blame as any).creationMethod !== "IN_PERSON"
|
||||||
|
) {
|
||||||
throw new BadRequestException({
|
throw new BadRequestException({
|
||||||
success: false,
|
success: false,
|
||||||
status: "unavailable" satisfies ExpertFileAssignStatus,
|
status: "unavailable" satisfies ExpertFileAssignStatus,
|
||||||
message: "Only V4 FileMaker files can be assigned to a FileReviewer.",
|
message: "Only V4/V5 FileMaker files can be assigned to a FileReviewer.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if ((blame as any).status !== "WAITING_FOR_FILE_REVIEWER") {
|
if ((blame as any).status !== "WAITING_FOR_FILE_REVIEWER") {
|
||||||
@@ -2674,6 +2684,12 @@ export class ExpertClaimService {
|
|||||||
if (!reviewerBlame) {
|
if (!reviewerBlame) {
|
||||||
throw new NotFoundException("Linked blame file not found.");
|
throw new NotFoundException("Linked blame file not found.");
|
||||||
}
|
}
|
||||||
|
const clientKey = requireActorClientKey(actor);
|
||||||
|
if (!blameCaseTouchesClient(reviewerBlame, clientKey)) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"This file does not belong to your organization.",
|
||||||
|
);
|
||||||
|
}
|
||||||
const assignedReviewerId = (reviewerBlame as any).assignedFileReviewerId
|
const assignedReviewerId = (reviewerBlame as any).assignedFileReviewerId
|
||||||
? String((reviewerBlame as any).assignedFileReviewerId)
|
? String((reviewerBlame as any).assignedFileReviewerId)
|
||||||
: null;
|
: null;
|
||||||
@@ -4773,6 +4789,12 @@ export class ExpertClaimService {
|
|||||||
"FileReviewers can only access V4/V5 FileMaker files.",
|
"FileReviewers can only access V4/V5 FileMaker files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const clientKey = requireActorClientKey(actor);
|
||||||
|
if (!blameCaseTouchesClient(linkedBlame, clientKey)) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"This file does not belong to your organization.",
|
||||||
|
);
|
||||||
|
}
|
||||||
const assignedReviewerId = (linkedBlame as any)?.assignedFileReviewerId
|
const assignedReviewerId = (linkedBlame as any)?.assignedFileReviewerId
|
||||||
? String((linkedBlame as any).assignedFileReviewerId)
|
? String((linkedBlame as any).assignedFileReviewerId)
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -86,8 +86,9 @@ export class FileReviewerBlameV4Controller {
|
|||||||
|
|
||||||
@Get("my-files")
|
@Get("my-files")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "List all blame files assigned to this FileReviewer",
|
summary: "List available and assigned FileMaker blame files",
|
||||||
description: "Returns all V4 FileMaker blame files that have been assigned to the authenticated FileReviewer.",
|
description:
|
||||||
|
"Returns V4 FileMaker blame files in this reviewer's insurer: sealed files that are still available to claim, plus files already assigned to the authenticated FileReviewer.",
|
||||||
})
|
})
|
||||||
async getMyFiles(@CurrentUser() fileReviewer: any) {
|
async getMyFiles(@CurrentUser() fileReviewer: any) {
|
||||||
return this.requestManagementService.getMyFileReviewerFiles(fileReviewer);
|
return this.requestManagementService.getMyFileReviewerFiles(fileReviewer);
|
||||||
@@ -96,8 +97,9 @@ export class FileReviewerBlameV4Controller {
|
|||||||
@Get("my-files/:requestId")
|
@Get("my-files/:requestId")
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get a single blame file assigned to this FileReviewer",
|
summary: "Get one available or assigned FileMaker blame file",
|
||||||
description: "Returns full detail — parties, workflow, expert fields, linked claim ID — for one V4 blame file.",
|
description:
|
||||||
|
"Returns full detail for a V4 FileMaker blame file in this reviewer's insurer when it is still available to claim or is assigned to the authenticated FileReviewer.",
|
||||||
})
|
})
|
||||||
async getMyFileDetail(
|
async getMyFileDetail(
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
|
|||||||
@@ -84,8 +84,9 @@ export class FileReviewerBlameV5Controller {
|
|||||||
|
|
||||||
@Get("my-files")
|
@Get("my-files")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "List all blame files assigned to this FileReviewer",
|
summary: "List available and assigned FileMaker blame files",
|
||||||
description: "Returns all V5 FileMaker blame files that have been assigned to the authenticated FileReviewer.",
|
description:
|
||||||
|
"Returns V5 FileMaker blame files in this reviewer's insurer: sealed files that are still available to claim, plus files already assigned to the authenticated FileReviewer.",
|
||||||
})
|
})
|
||||||
async getMyFiles(@CurrentUser() fileReviewer: any) {
|
async getMyFiles(@CurrentUser() fileReviewer: any) {
|
||||||
return this.requestManagementService.getMyFileReviewerFiles(fileReviewer);
|
return this.requestManagementService.getMyFileReviewerFiles(fileReviewer);
|
||||||
@@ -94,8 +95,9 @@ export class FileReviewerBlameV5Controller {
|
|||||||
@Get("my-files/:requestId")
|
@Get("my-files/:requestId")
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Get a single blame file assigned to this FileReviewer",
|
summary: "Get one available or assigned FileMaker blame file",
|
||||||
description: "Returns full detail — parties, workflow, expert fields, linked claim ID — for one V5 blame file.",
|
description:
|
||||||
|
"Returns full detail for a V5 FileMaker blame file in this reviewer's insurer when it is still available to claim or is assigned to the authenticated FileReviewer.",
|
||||||
})
|
})
|
||||||
async getMyFileDetail(
|
async getMyFileDetail(
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
|
|||||||
140
src/request-management/request-management.file-reviewer.spec.ts
Normal file
140
src/request-management/request-management.file-reviewer.spec.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
|
import { ExpertClaimService } from "src/expert-claim/expert-claim.service";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
import { RequestManagementService } from "./request-management.service";
|
||||||
|
|
||||||
|
describe("RequestManagementService FileReviewer inbox", () => {
|
||||||
|
const reviewerId = new Types.ObjectId();
|
||||||
|
const otherReviewerId = new Types.ObjectId();
|
||||||
|
const clientId = new Types.ObjectId();
|
||||||
|
const otherClientId = new Types.ObjectId();
|
||||||
|
|
||||||
|
const sealedFile = {
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
publicId: "BLM-OPEN",
|
||||||
|
type: "THIRD_PARTY",
|
||||||
|
status: "WAITING_FOR_FILE_REVIEWER",
|
||||||
|
isMadeByFileMaker: true,
|
||||||
|
expertInitiated: true,
|
||||||
|
creationMethod: "IN_PERSON",
|
||||||
|
parties: [
|
||||||
|
{
|
||||||
|
role: "FIRST",
|
||||||
|
person: { clientId, userId: new Types.ObjectId() },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
expert: { decision: { guiltyPartyId: new Types.ObjectId() } },
|
||||||
|
};
|
||||||
|
|
||||||
|
function createService(files: any[]) {
|
||||||
|
const blameRequestDbService = {
|
||||||
|
find: jest.fn().mockResolvedValue(files),
|
||||||
|
};
|
||||||
|
const service = new (RequestManagementService as any)(
|
||||||
|
undefined,
|
||||||
|
blameRequestDbService,
|
||||||
|
) as RequestManagementService;
|
||||||
|
return { service, blameRequestDbService };
|
||||||
|
}
|
||||||
|
|
||||||
|
it("lists a FileMaker-sealed, unassigned file for a reviewer in the same tenant", async () => {
|
||||||
|
const { service, blameRequestDbService } = createService([sealedFile]);
|
||||||
|
|
||||||
|
const result = await service.getMyFileReviewerFiles({
|
||||||
|
sub: String(reviewerId),
|
||||||
|
role: RoleEnum.FILE_REVIEWER,
|
||||||
|
clientKey: String(clientId),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
expect.objectContaining({ _id: sealedFile._id, publicId: "BLM-OPEN" }),
|
||||||
|
]);
|
||||||
|
expect(blameRequestDbService.find).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
isMadeByFileMaker: true,
|
||||||
|
expertInitiated: true,
|
||||||
|
creationMethod: "IN_PERSON",
|
||||||
|
$or: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ status: "WAITING_FOR_FILE_REVIEWER" }),
|
||||||
|
expect.objectContaining({ assignedFileReviewerId: reviewerId }),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not list another tenant's open file or a file assigned to another reviewer", async () => {
|
||||||
|
const { service } = createService([
|
||||||
|
{
|
||||||
|
...sealedFile,
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
parties: [{ role: "FIRST", person: { clientId: otherClientId } }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...sealedFile,
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
assignedFileReviewerId: otherReviewerId,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.getMyFileReviewerFiles({
|
||||||
|
sub: String(reviewerId),
|
||||||
|
role: RoleEnum.FILE_REVIEWER,
|
||||||
|
clientKey: String(clientId),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not expose an open file's details to a reviewer from another tenant", async () => {
|
||||||
|
const { service } = createService([]);
|
||||||
|
(service as any).blameRequestDbService.findById = jest.fn().mockResolvedValue({
|
||||||
|
...sealedFile,
|
||||||
|
parties: [{ role: "FIRST", person: { clientId: otherClientId } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.getMyFileReviewerFileDetail(
|
||||||
|
{
|
||||||
|
sub: String(reviewerId),
|
||||||
|
role: RoleEnum.FILE_REVIEWER,
|
||||||
|
clientKey: String(clientId),
|
||||||
|
},
|
||||||
|
String(sealedFile._id),
|
||||||
|
),
|
||||||
|
).rejects.toThrow("does not belong to your organization");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not let a reviewer claim another tenant's file through its linked claim ID", async () => {
|
||||||
|
const blameRequestDbService = {
|
||||||
|
findById: jest.fn().mockResolvedValue({
|
||||||
|
...sealedFile,
|
||||||
|
parties: [{ role: "FIRST", person: { clientId: otherClientId } }],
|
||||||
|
}),
|
||||||
|
findOneAndUpdate: jest.fn(),
|
||||||
|
};
|
||||||
|
const expertClaimService = new (ExpertClaimService as any)(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
blameRequestDbService,
|
||||||
|
) as ExpertClaimService;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(expertClaimService as any).assignFileReviewerToV4Blame(
|
||||||
|
String(new Types.ObjectId()),
|
||||||
|
{ blameRequestId: sealedFile._id },
|
||||||
|
{
|
||||||
|
sub: String(reviewerId),
|
||||||
|
role: RoleEnum.FILE_REVIEWER,
|
||||||
|
clientKey: String(clientId),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).rejects.toThrow("does not belong to your organization");
|
||||||
|
expect(blameRequestDbService.findOneAndUpdate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -109,6 +109,10 @@ import {
|
|||||||
buildBlamePartyAccessOrConditions,
|
buildBlamePartyAccessOrConditions,
|
||||||
collectUserIdVariants,
|
collectUserIdVariants,
|
||||||
} from "src/helpers/party-access-queries";
|
} from "src/helpers/party-access-queries";
|
||||||
|
import {
|
||||||
|
blameCaseTouchesClient,
|
||||||
|
requireActorClientKey,
|
||||||
|
} from "src/helpers/tenant-scope";
|
||||||
import { resolveLinkedUserIdStrings } from "src/helpers/user-access-resolver";
|
import { resolveLinkedUserIdStrings } from "src/helpers/user-access-resolver";
|
||||||
import { normalizePlateText } from "src/utils/plate-normalizer/plate-normalizer.service";
|
import { normalizePlateText } from "src/utils/plate-normalizer/plate-normalizer.service";
|
||||||
|
|
||||||
@@ -4671,6 +4675,12 @@ export class RequestManagementService {
|
|||||||
"FileReviewer can only access V4/V5 FileMaker files.",
|
"FileReviewer can only access V4/V5 FileMaker files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const clientKey = requireActorClientKey(expert);
|
||||||
|
if (!blameCaseTouchesClient(req, clientKey)) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"This file does not belong to your organization.",
|
||||||
|
);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
req.status !== CaseStatus.WAITING_FOR_FILE_REVIEWER &&
|
req.status !== CaseStatus.WAITING_FOR_FILE_REVIEWER &&
|
||||||
req.status !== CaseStatus.WAITING_FOR_EXPERT &&
|
req.status !== CaseStatus.WAITING_FOR_EXPERT &&
|
||||||
@@ -11415,12 +11425,41 @@ export class RequestManagementService {
|
|||||||
if (fileReviewer?.role !== RoleEnum.FILE_REVIEWER) {
|
if (fileReviewer?.role !== RoleEnum.FILE_REVIEWER) {
|
||||||
throw new ForbiddenException("Only FileReviewers can use this endpoint.");
|
throw new ForbiddenException("Only FileReviewers can use this endpoint.");
|
||||||
}
|
}
|
||||||
|
const clientKey = requireActorClientKey(fileReviewer);
|
||||||
const reviewerId = new Types.ObjectId(fileReviewer.sub);
|
const reviewerId = new Types.ObjectId(fileReviewer.sub);
|
||||||
const files = await this.blameRequestDbService.find({
|
const files = await this.blameRequestDbService.find({
|
||||||
isMadeByFileMaker: true,
|
isMadeByFileMaker: true,
|
||||||
assignedFileReviewerId: reviewerId,
|
expertInitiated: true,
|
||||||
|
creationMethod: CreationMethod.IN_PERSON,
|
||||||
|
$or: [
|
||||||
|
// A FileMaker-sealed file must be discoverable before a reviewer can
|
||||||
|
// claim it. Once another reviewer takes it, only that reviewer sees it.
|
||||||
|
{
|
||||||
|
status: CaseStatus.WAITING_FOR_FILE_REVIEWER,
|
||||||
|
$or: [
|
||||||
|
{ assignedFileReviewerId: { $exists: false } },
|
||||||
|
{ assignedFileReviewerId: null },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ assignedFileReviewerId: reviewerId },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
return (files || []).map((f: any) => ({
|
const visibleFiles = (files || []).filter((file: any) => {
|
||||||
|
const assignedReviewerId = file.assignedFileReviewerId
|
||||||
|
? String(file.assignedFileReviewerId)
|
||||||
|
: null;
|
||||||
|
const isOpen =
|
||||||
|
file.status === CaseStatus.WAITING_FOR_FILE_REVIEWER &&
|
||||||
|
!assignedReviewerId;
|
||||||
|
const isAssignedToReviewer =
|
||||||
|
assignedReviewerId === String(fileReviewer.sub);
|
||||||
|
return (
|
||||||
|
blameCaseTouchesClient(file, clientKey) &&
|
||||||
|
(isOpen || isAssignedToReviewer)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return visibleFiles.map((f: any) => ({
|
||||||
_id: f._id,
|
_id: f._id,
|
||||||
publicId: f.publicId,
|
publicId: f.publicId,
|
||||||
requestNo: f.requestNo,
|
requestNo: f.requestNo,
|
||||||
@@ -11442,17 +11481,33 @@ export class RequestManagementService {
|
|||||||
if (fileReviewer?.role !== RoleEnum.FILE_REVIEWER) {
|
if (fileReviewer?.role !== RoleEnum.FILE_REVIEWER) {
|
||||||
throw new ForbiddenException("Only FileReviewers can use this endpoint.");
|
throw new ForbiddenException("Only FileReviewers can use this endpoint.");
|
||||||
}
|
}
|
||||||
|
const clientKey = requireActorClientKey(fileReviewer);
|
||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const req = await this.blameRequestDbService.findById(requestId);
|
||||||
if (!req) throw new NotFoundException("Blame request not found");
|
if (!req) throw new NotFoundException("Blame request not found");
|
||||||
if (!req.isMadeByFileMaker) {
|
if (
|
||||||
|
!req.isMadeByFileMaker ||
|
||||||
|
!req.expertInitiated ||
|
||||||
|
req.creationMethod !== CreationMethod.IN_PERSON
|
||||||
|
) {
|
||||||
throw new ForbiddenException("FileReviewer can only access V4/V5 FileMaker files.");
|
throw new ForbiddenException("FileReviewer can only access V4/V5 FileMaker files.");
|
||||||
}
|
}
|
||||||
|
if (!blameCaseTouchesClient(req, clientKey)) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"This file does not belong to your organization.",
|
||||||
|
);
|
||||||
|
}
|
||||||
const assignedId = (req as any).assignedFileReviewerId
|
const assignedId = (req as any).assignedFileReviewerId
|
||||||
? String((req as any).assignedFileReviewerId)
|
? String((req as any).assignedFileReviewerId)
|
||||||
: null;
|
: null;
|
||||||
if (assignedId && assignedId !== String(fileReviewer.sub)) {
|
if (assignedId && assignedId !== String(fileReviewer.sub)) {
|
||||||
throw new ForbiddenException("This file has been taken by another FileReviewer.");
|
throw new ForbiddenException("This file has been taken by another FileReviewer.");
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
!assignedId &&
|
||||||
|
req.status !== CaseStatus.WAITING_FOR_FILE_REVIEWER
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException("This file is not available for review.");
|
||||||
|
}
|
||||||
const plain = typeof (req as any).toObject === "function"
|
const plain = typeof (req as any).toObject === "function"
|
||||||
? (req as any).toObject({ versionKey: false })
|
? (req as any).toObject({ versionKey: false })
|
||||||
: { ...(req as any) };
|
: { ...(req as any) };
|
||||||
|
|||||||
Reference in New Issue
Block a user