forked from Yara724/api
Merge pull request 'YARA-1110' (#186) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#186
This commit is contained in:
@@ -18,17 +18,16 @@ export enum CaseStatus {
|
|||||||
WAITING_FOR_FILE_REVIEWER = "WAITING_FOR_FILE_REVIEWER",
|
WAITING_FOR_FILE_REVIEWER = "WAITING_FOR_FILE_REVIEWER",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V5 flow only. FileReviewer has uploaded the blame accident video;
|
* V5 flow only. FileReviewer has completed the claim and the owner has signed;
|
||||||
* the file is now awaiting FinancialExpert approval before moving
|
* the FileMaker who created the file must now approve before fanavaran submission.
|
||||||
* to WAITING_FOR_EXPERT (fanavaran submission).
|
|
||||||
*/
|
*/
|
||||||
WAITING_FOR_FINANCIAL_EXPERT = "WAITING_FOR_FINANCIAL_EXPERT",
|
WAITING_FOR_FILE_MAKER_APPROVAL = "WAITING_FOR_FILE_MAKER_APPROVAL",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V5 flow only. FinancialExpert rejected the file back to FileReviewer
|
* V5 flow only. FileMaker rejected the file back to FileReviewer
|
||||||
* for correction (adjust data or send back to user and re-submit).
|
* for correction (adjust pricing / back-and-forth with user and re-submit).
|
||||||
*/
|
*/
|
||||||
FINANCIAL_EXPERT_REJECTED = "FINANCIAL_EXPERT_REJECTED",
|
FILE_MAKER_REJECTED = "FILE_MAKER_REJECTED",
|
||||||
|
|
||||||
COMPLETED = "COMPLETED",
|
COMPLETED = "COMPLETED",
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,18 @@ export enum ClaimCaseStatus {
|
|||||||
*/
|
*/
|
||||||
WAITING_FOR_FILE_REVIEWER = "WAITING_FOR_FILE_REVIEWER",
|
WAITING_FOR_FILE_REVIEWER = "WAITING_FOR_FILE_REVIEWER",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V5 split flow only. The claim is fully evaluated and owner has signed;
|
||||||
|
* the FileMaker who created the file must approve before fanavaran submission.
|
||||||
|
*/
|
||||||
|
WAITING_FOR_FILE_MAKER_APPROVAL = "WAITING_FOR_FILE_MAKER_APPROVAL",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V5 split flow only. FileMaker rejected the completed claim back to FileReviewer
|
||||||
|
* for correction (adjust pricing, re-do expert review, back-and-forth with user).
|
||||||
|
*/
|
||||||
|
FILE_MAKER_REJECTED = "FILE_MAKER_REJECTED",
|
||||||
|
|
||||||
// Final states
|
// Final states
|
||||||
COMPLETED = "COMPLETED",
|
COMPLETED = "COMPLETED",
|
||||||
CANCELLED = "CANCELLED",
|
CANCELLED = "CANCELLED",
|
||||||
|
|||||||
@@ -9,5 +9,4 @@ export enum RoleEnum {
|
|||||||
FILE_MAKER = "file_maker",
|
FILE_MAKER = "file_maker",
|
||||||
FILE_REVIEWER = "file_reviewer",
|
FILE_REVIEWER = "file_reviewer",
|
||||||
SUPER_ADMIN = "super_admin",
|
SUPER_ADMIN = "super_admin",
|
||||||
FINANCIAL_EXPERT = "financial_expert",
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import { FieldExpertDbService } from "src/users/entities/db-service/field-expert
|
|||||||
import { RegistrarDbService } from "src/users/entities/db-service/registrar.db.service";
|
import { RegistrarDbService } from "src/users/entities/db-service/registrar.db.service";
|
||||||
import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service";
|
import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service";
|
||||||
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
|
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
|
||||||
import { FinancialExpertDbService } from "src/users/entities/db-service/financial-expert.db.service";
|
|
||||||
import { HashService } from "src/utils/hash/hash.service";
|
import { HashService } from "src/utils/hash/hash.service";
|
||||||
import { OtpGeneratorService } from "src/sms-orchestration/otp-generator.service";
|
import { OtpGeneratorService } from "src/sms-orchestration/otp-generator.service";
|
||||||
import { SuperAdminDbService } from "src/super-admin/entities/db-service/super-admin.db.service";
|
import { SuperAdminDbService } from "src/super-admin/entities/db-service/super-admin.db.service";
|
||||||
@@ -58,7 +57,6 @@ export class ActorAuthService {
|
|||||||
private readonly captchaChallengeService: CaptchaChallengeService,
|
private readonly captchaChallengeService: CaptchaChallengeService,
|
||||||
private readonly fileMakerDbService: FileMakerDbService,
|
private readonly fileMakerDbService: FileMakerDbService,
|
||||||
private readonly fileReviewerDbService: FileReviewerDbService,
|
private readonly fileReviewerDbService: FileReviewerDbService,
|
||||||
private readonly financialExpertDbService: FinancialExpertDbService,
|
|
||||||
private readonly superAdminDbService: SuperAdminDbService,
|
private readonly superAdminDbService: SuperAdminDbService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -118,15 +116,6 @@ export class ActorAuthService {
|
|||||||
res =
|
res =
|
||||||
await this.fileReviewerDbService.findByLoginIdentifier(username);
|
await this.fileReviewerDbService.findByLoginIdentifier(username);
|
||||||
break;
|
break;
|
||||||
case RoleEnum.FINANCIAL_EXPERT:
|
|
||||||
if (username == null && userId)
|
|
||||||
res = await this.financialExpertDbService.findOne({
|
|
||||||
_id: new Types.ObjectId(userId),
|
|
||||||
});
|
|
||||||
else
|
|
||||||
res =
|
|
||||||
await this.financialExpertDbService.findByLoginIdentifier(username);
|
|
||||||
break;
|
|
||||||
case RoleEnum.SUPER_ADMIN:
|
case RoleEnum.SUPER_ADMIN:
|
||||||
if (username == null && userId)
|
if (username == null && userId)
|
||||||
res = await this.superAdminDbService.findOne({
|
res = await this.superAdminDbService.findOne({
|
||||||
|
|||||||
@@ -6167,6 +6167,30 @@ export class ClaimRequestManagementService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// V5 flow: FileMaker approval required before fanavaran.
|
||||||
|
// Instead of submitting, hold the claim at WAITING_FOR_FILE_MAKER_APPROVAL.
|
||||||
|
if ((claimCase as any).requiresFileMakerApproval) {
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(claimCaseId, {
|
||||||
|
$set: { status: ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL },
|
||||||
|
$push: {
|
||||||
|
history: {
|
||||||
|
type: "V5_HELD_FOR_FILE_MAKER_APPROVAL",
|
||||||
|
actor: { actorType: "system" },
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: { claimCaseId },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
attempted: false,
|
||||||
|
submitted: false,
|
||||||
|
skipped: true,
|
||||||
|
skipReason:
|
||||||
|
"V5 flow: FileMaker approval required before Fanavaran submission. " +
|
||||||
|
"Claim is now in WAITING_FOR_FILE_MAKER_APPROVAL.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const skipReason = await this.getFanavaranAutoSubmitSkipReason(claimCase);
|
const skipReason = await this.getFanavaranAutoSubmitSkipReason(claimCase);
|
||||||
if (skipReason) {
|
if (skipReason) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -242,6 +242,20 @@ export class ClaimCase {
|
|||||||
@Prop({ type: Types.ObjectId, index: true })
|
@Prop({ type: Types.ObjectId, index: true })
|
||||||
createdByRegistrarId?: Types.ObjectId;
|
createdByRegistrarId?: Types.ObjectId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V5 split flow: when true, the claim must be approved by the FileMaker
|
||||||
|
* who created the file before fanavaran submission is allowed.
|
||||||
|
*/
|
||||||
|
@Prop({ type: Boolean, default: false })
|
||||||
|
requiresFileMakerApproval?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V5 split flow: ObjectId of the FileMaker who must approve this claim.
|
||||||
|
* Set when the FileReviewer uploads the blame accident video in the V5 flow.
|
||||||
|
*/
|
||||||
|
@Prop({ type: Types.ObjectId, index: true })
|
||||||
|
fileMakerApprovalActorId?: Types.ObjectId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy fields kept optional to simplify progressive migration.
|
* Legacy fields kept optional to simplify progressive migration.
|
||||||
* If you choose to migrate later, we can remove these.
|
* If you choose to migrate later, we can remove these.
|
||||||
|
|||||||
@@ -388,6 +388,12 @@ export class ExpertClaimService {
|
|||||||
claim: any,
|
claim: any,
|
||||||
actor: any,
|
actor: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
// FILE_REVIEWER: by the time we reach this method the caller has already
|
||||||
|
// verified that actor.sub === blame.assignedFileReviewerId (Phase 2 of
|
||||||
|
// assignClaimForReviewV2). The generic tenant-scope check would incorrectly
|
||||||
|
// reject them because initiatedByFieldExpertId belongs to the FileMaker, not
|
||||||
|
// the reviewer. Skip it — the assignment check is the access proof.
|
||||||
|
if ((actor as any).role === RoleEnum.FILE_REVIEWER) return;
|
||||||
const blame = await this.loadBlameForClaim(claim);
|
const blame = await this.loadBlameForClaim(claim);
|
||||||
assertClaimCaseForExpertActor(claim, actor, blame);
|
assertClaimCaseForExpertActor(claim, actor, blame);
|
||||||
}
|
}
|
||||||
@@ -2635,11 +2641,55 @@ export class ExpertClaimService {
|
|||||||
throw new NotFoundException("Claim request not found");
|
throw new NotFoundException("Claim request not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// FILE_REVIEWER: assign themselves to the linked blame (V4 flow).
|
// FILE_REVIEWER: two-phase lock behaviour.
|
||||||
// This is a different path from the damage-expert lock — it operates on the
|
// Phase 1 — blame is WAITING_FOR_FILE_REVIEWER: atomically set
|
||||||
// blame document and is idempotent (same reviewer can re-assign to themselves).
|
// assignedFileReviewerId on the blame document.
|
||||||
|
// Phase 2 — blame is already past WAITING_FOR_FILE_REVIEWER (i.e. the
|
||||||
|
// reviewer finished field work and blame moved to WAITING_FOR_EXPERT):
|
||||||
|
// the reviewer now needs the standard damage-expert workflow lock
|
||||||
|
// so they can perform damage assessment. Fall through to the
|
||||||
|
// damage-expert path below; assertExpertActorOnClaim will verify
|
||||||
|
// tenant scope via the reviewer's clientKey.
|
||||||
if ((actor as any).role === RoleEnum.FILE_REVIEWER) {
|
if ((actor as any).role === RoleEnum.FILE_REVIEWER) {
|
||||||
return this.assignFileReviewerToV4Blame(claimRequestId, claim, actor);
|
if (!claim.blameRequestId) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
success: false,
|
||||||
|
status: "unavailable" satisfies ExpertFileAssignStatus,
|
||||||
|
message: "This claim has no linked blame file.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const reviewerBlame = await this.blameRequestDbService.findById(
|
||||||
|
String(claim.blameRequestId),
|
||||||
|
);
|
||||||
|
if (!reviewerBlame) {
|
||||||
|
throw new NotFoundException("Linked blame file not found.");
|
||||||
|
}
|
||||||
|
const blameStatus = (reviewerBlame as any).status as string;
|
||||||
|
if (blameStatus === "WAITING_FOR_FILE_REVIEWER") {
|
||||||
|
// Phase 1: blame-assign path
|
||||||
|
return this.assignFileReviewerToV4Blame(claimRequestId, claim, actor);
|
||||||
|
}
|
||||||
|
// Phase 2: blame is past the initial assignment step.
|
||||||
|
// Only the reviewer who was assigned during Phase 1 may proceed.
|
||||||
|
const assignedReviewerId = (reviewerBlame as any).assignedFileReviewerId
|
||||||
|
? String((reviewerBlame as any).assignedFileReviewerId)
|
||||||
|
: null;
|
||||||
|
if (!assignedReviewerId) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
success: false,
|
||||||
|
status: "unavailable" satisfies ExpertFileAssignStatus,
|
||||||
|
message: "No reviewer has been assigned to this file yet.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (assignedReviewerId !== actor.sub) {
|
||||||
|
throw new ConflictException({
|
||||||
|
success: false,
|
||||||
|
status: "locked" satisfies ExpertFileAssignStatus,
|
||||||
|
message: "This file is assigned to another reviewer.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Assigned reviewer — fall through to the damage-expert workflow lock below.
|
||||||
|
// (actor.role stays FILE_REVIEWER; assertExpertActorOnClaim checks clientKey scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.assertExpertActorOnClaim(claim, actor);
|
await this.assertExpertActorOnClaim(claim, actor);
|
||||||
@@ -4613,7 +4663,8 @@ export class ExpertClaimService {
|
|||||||
const actorId = actor.sub;
|
const actorId = actor.sub;
|
||||||
if (
|
if (
|
||||||
actor.role !== RoleEnum.FIELD_EXPERT &&
|
actor.role !== RoleEnum.FIELD_EXPERT &&
|
||||||
actor.role !== RoleEnum.FILE_MAKER
|
actor.role !== RoleEnum.FILE_MAKER &&
|
||||||
|
actor.role !== RoleEnum.FILE_REVIEWER
|
||||||
) {
|
) {
|
||||||
await this.reconcileStaleExpertReviewingClaimLocksForTenant(actor);
|
await this.reconcileStaleExpertReviewingClaimLocksForTenant(actor);
|
||||||
}
|
}
|
||||||
@@ -4625,7 +4676,12 @@ export class ExpertClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const linkedBlame = await this.loadBlameForClaim(claim);
|
const linkedBlame = await this.loadBlameForClaim(claim);
|
||||||
assertClaimCaseForExpertActor(claim, actor, linkedBlame);
|
// FILE_REVIEWER access is validated by the role-specific gate below
|
||||||
|
// (isAssignedToMe || isOpen). Skip the generic tenant-scope assert which
|
||||||
|
// would incorrectly reject them via the initiatedByFieldExpertId path.
|
||||||
|
if (actor.role !== RoleEnum.FILE_REVIEWER) {
|
||||||
|
assertClaimCaseForExpertActor(claim, actor, linkedBlame);
|
||||||
|
}
|
||||||
|
|
||||||
// Variables used both in the gate block and in the detail-build section below
|
// Variables used both in the gate block and in the detail-build section below
|
||||||
const isDamageExpertPhase =
|
const isDamageExpertPhase =
|
||||||
@@ -4650,15 +4706,15 @@ export class ExpertClaimService {
|
|||||||
actor.role === RoleEnum.FILE_REVIEWER
|
actor.role === RoleEnum.FILE_REVIEWER
|
||||||
) {
|
) {
|
||||||
if (actor.role === RoleEnum.FILE_REVIEWER) {
|
if (actor.role === RoleEnum.FILE_REVIEWER) {
|
||||||
// FILE_REVIEWER: must be a V4 file AND (they are the assigned reviewer OR
|
// FILE_REVIEWER: must be a V4/V5 file AND (they are the assigned reviewer OR
|
||||||
// the file is still open — WAITING_FOR_FILE_REVIEWER with no reviewer yet).
|
// the file is still open — WAITING_FOR_FILE_REVIEWER with no reviewer yet).
|
||||||
const isV4Blame =
|
const isV4V5Blame =
|
||||||
(linkedBlame as any)?.isMadeByFileMaker &&
|
(linkedBlame as any)?.isMadeByFileMaker &&
|
||||||
linkedBlame?.expertInitiated &&
|
linkedBlame?.expertInitiated &&
|
||||||
linkedBlame?.creationMethod === "IN_PERSON";
|
linkedBlame?.creationMethod === "IN_PERSON";
|
||||||
if (!isV4Blame) {
|
if (!isV4V5Blame) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
"FileReviewers can only access V4 FileMaker files.",
|
"FileReviewers can only access V4/V5 FileMaker files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const assignedReviewerId = (linkedBlame as any)?.assignedFileReviewerId
|
const assignedReviewerId = (linkedBlame as any)?.assignedFileReviewerId
|
||||||
|
|||||||
@@ -97,11 +97,3 @@ export class CreateFileReviewerByInsurerDto extends CreateInsurerExpertDto {
|
|||||||
})
|
})
|
||||||
role?: RoleEnum.FILE_REVIEWER;
|
role?: RoleEnum.FILE_REVIEWER;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateFinancialExpertByInsurerDto extends CreateInsurerExpertDto {
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
enum: [RoleEnum.FINANCIAL_EXPERT],
|
|
||||||
default: RoleEnum.FINANCIAL_EXPERT,
|
|
||||||
})
|
|
||||||
role?: RoleEnum.FINANCIAL_EXPERT;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import {
|
|||||||
CreateClaimExpertByInsurerDto,
|
CreateClaimExpertByInsurerDto,
|
||||||
CreateFileMakerByInsurerDto,
|
CreateFileMakerByInsurerDto,
|
||||||
CreateFileReviewerByInsurerDto,
|
CreateFileReviewerByInsurerDto,
|
||||||
CreateFinancialExpertByInsurerDto,
|
|
||||||
} from "./dto/create-insurer-expert.dto";
|
} from "./dto/create-insurer-expert.dto";
|
||||||
|
|
||||||
@Controller("expert-insurer")
|
@Controller("expert-insurer")
|
||||||
@@ -183,23 +182,6 @@ export class ExpertInsurerController {
|
|||||||
return this.expertInsurerService.addFileReviewer(insurer.clientKey, body);
|
return this.expertInsurerService.addFileReviewer(insurer.clientKey, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("experts/financial-expert")
|
|
||||||
@ApiBody({ type: CreateFinancialExpertByInsurerDto })
|
|
||||||
@ApiOperation({
|
|
||||||
summary: "Create a FinancialExpert account under this insurer (V5 flow)",
|
|
||||||
description:
|
|
||||||
"FinancialExperts are the final approvers in the V5 blame flow before fanavaran submission.",
|
|
||||||
})
|
|
||||||
async addFinancialExpert(
|
|
||||||
@CurrentUser() insurer,
|
|
||||||
@Body() body: CreateFinancialExpertByInsurerDto,
|
|
||||||
) {
|
|
||||||
if (!insurer) {
|
|
||||||
throw new UnauthorizedException("Could not identify the current user.");
|
|
||||||
}
|
|
||||||
return this.expertInsurerService.addFinancialExpert(insurer.clientKey, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiQuery({ name: "page", type: Number })
|
@ApiQuery({ name: "page", type: Number })
|
||||||
@ApiQuery({ name: "response_count", type: Number })
|
@ApiQuery({ name: "response_count", type: Number })
|
||||||
@Get("experts/list")
|
@Get("experts/list")
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
CreateClaimExpertByInsurerDto,
|
CreateClaimExpertByInsurerDto,
|
||||||
CreateFileMakerByInsurerDto,
|
CreateFileMakerByInsurerDto,
|
||||||
CreateFileReviewerByInsurerDto,
|
CreateFileReviewerByInsurerDto,
|
||||||
CreateFinancialExpertByInsurerDto,
|
|
||||||
CreateInsurerExpertDto,
|
CreateInsurerExpertDto,
|
||||||
} from "./dto/create-insurer-expert.dto";
|
} from "./dto/create-insurer-expert.dto";
|
||||||
import {
|
import {
|
||||||
@@ -30,7 +29,6 @@ import { ExpertDbService } from "src/users/entities/db-service/expert.db.service
|
|||||||
import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service";
|
import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service";
|
||||||
import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service";
|
import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service";
|
||||||
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
|
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
|
||||||
import { FinancialExpertDbService } from "src/users/entities/db-service/financial-expert.db.service";
|
|
||||||
import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service";
|
import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service";
|
||||||
import { ExpertFileActivityType } from "src/users/entities/schema/expert-file-activity.schema";
|
import { ExpertFileActivityType } from "src/users/entities/schema/expert-file-activity.schema";
|
||||||
import { HashService } from "src/utils/hash/hash.service";
|
import { HashService } from "src/utils/hash/hash.service";
|
||||||
@@ -97,7 +95,6 @@ export class ExpertInsurerService {
|
|||||||
private readonly claimSignDbService: ClaimSignDbService,
|
private readonly claimSignDbService: ClaimSignDbService,
|
||||||
private readonly fileMakerDbService: FileMakerDbService,
|
private readonly fileMakerDbService: FileMakerDbService,
|
||||||
private readonly fileReviewerDbService: FileReviewerDbService,
|
private readonly fileReviewerDbService: FileReviewerDbService,
|
||||||
private readonly financialExpertDbService: FinancialExpertDbService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1769,58 +1766,6 @@ export class ExpertInsurerService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFinancialExpert(
|
|
||||||
insurerClientKey: string,
|
|
||||||
payload: CreateFinancialExpertByInsurerDto,
|
|
||||||
) {
|
|
||||||
const clientObjectId = this.getClientId(insurerClientKey);
|
|
||||||
const branch = await this.assertBranchBelongsToClient(
|
|
||||||
payload.branchId,
|
|
||||||
clientObjectId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const email = payload.email.trim().toLowerCase();
|
|
||||||
const existing = await this.financialExpertDbService.findOne({ email });
|
|
||||||
if (existing) {
|
|
||||||
throw new ConflictException(
|
|
||||||
"A FinancialExpert with this email already exists.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nationalCode = payload.nationalCode.trim();
|
|
||||||
const existingByNational = await this.financialExpertDbService.findOne({
|
|
||||||
nationalCode,
|
|
||||||
});
|
|
||||||
if (existingByNational) {
|
|
||||||
throw new ConflictException(
|
|
||||||
"A FinancialExpert with this national code already exists.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedPassword = await this.hashService.hash(payload.password);
|
|
||||||
const created = await this.financialExpertDbService.create({
|
|
||||||
...payload,
|
|
||||||
email,
|
|
||||||
nationalCode,
|
|
||||||
password: hashedPassword,
|
|
||||||
role: RoleEnum.FINANCIAL_EXPERT,
|
|
||||||
clientKey: clientObjectId,
|
|
||||||
branchId: new Types.ObjectId(payload.branchId),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
financialExpert: this.sanitizeExpertResponse(created),
|
|
||||||
branch: {
|
|
||||||
_id: (branch as any)._id,
|
|
||||||
name: branch.name,
|
|
||||||
code: branch.code,
|
|
||||||
city: branch.city,
|
|
||||||
state: branch.state,
|
|
||||||
address: branch.address,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get comprehensive statistics for all experts of a client
|
* Get comprehensive statistics for all experts of a client
|
||||||
* Returns:
|
* Returns:
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiBody,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
ApiTags,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { IsOptional, IsString } from "class-validator";
|
||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
|
import { Roles } from "src/decorators/roles.decorator";
|
||||||
|
import { CurrentUser } from "src/decorators/user.decorator";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
import { RequestManagementService } from "./request-management.service";
|
||||||
|
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
||||||
|
|
||||||
|
class FileMakerRejectDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Reason for rejection (visible to FileReviewer)",
|
||||||
|
example: "Damage assessment is incorrect — please re-evaluate parts X and Y",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V5 FileMaker approval panel.
|
||||||
|
*
|
||||||
|
* After the full claim flow completes (FileReviewer does damage assessment via
|
||||||
|
* expert-claim APIs, user signs), the claim lands in WAITING_FOR_FILE_MAKER_APPROVAL.
|
||||||
|
* The FileMaker who created the blame file can then:
|
||||||
|
*
|
||||||
|
* approve → triggers fanavaran submission (claim → COMPLETED)
|
||||||
|
* reject → sends claim back to WAITING_FOR_DAMAGE_EXPERT so the FileReviewer
|
||||||
|
* can re-lock, adjust pricing, and redo the user interaction
|
||||||
|
*
|
||||||
|
* All endpoints operate on `claimRequestId` (the claim case ID, not the blame ID).
|
||||||
|
* Use `GET v5/file-maker/blame-request-management/claim-id/:requestId` to obtain
|
||||||
|
* the claimRequestId from the original blame request ID.
|
||||||
|
*/
|
||||||
|
@ApiTags("v5 FileMaker — claim approval before fanavaran")
|
||||||
|
@Controller("v5/file-maker/claim-approval")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
|
@Roles(RoleEnum.FILE_MAKER)
|
||||||
|
export class FileMakerClaimApprovalV5Controller {
|
||||||
|
constructor(
|
||||||
|
private readonly requestManagementService: RequestManagementService,
|
||||||
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post("approve/:claimRequestId")
|
||||||
|
@ApiParam({ name: "claimRequestId" })
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Approve the completed claim (FileMaker V5)",
|
||||||
|
description:
|
||||||
|
"Approves a claim that is in WAITING_FOR_FILE_MAKER_APPROVAL status. " +
|
||||||
|
"Marks the claim COMPLETED and triggers fanavaran submission.",
|
||||||
|
})
|
||||||
|
async approve(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@CurrentUser() fileMaker: any,
|
||||||
|
) {
|
||||||
|
const result = await this.requestManagementService.fileMakerApproveV5(
|
||||||
|
fileMaker,
|
||||||
|
claimRequestId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trigger fanavaran auto-submit now that the claim is COMPLETED and the gate
|
||||||
|
// (requiresFileMakerApproval) has been cleared.
|
||||||
|
const fanavaran =
|
||||||
|
await this.claimRequestManagementService.autoSubmitToFanavaranV2OnClaimCompleted(
|
||||||
|
claimRequestId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...result, fanavaran };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("reject/:claimRequestId")
|
||||||
|
@ApiParam({ name: "claimRequestId" })
|
||||||
|
@ApiBody({ type: FileMakerRejectDto })
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Reject the completed claim back to FileReviewer (FileMaker V5)",
|
||||||
|
description:
|
||||||
|
"Rejects a claim that is in WAITING_FOR_FILE_MAKER_APPROVAL status. " +
|
||||||
|
"Moves claim to FILE_MAKER_REJECTED so the FileReviewer can re-lock, " +
|
||||||
|
"adjust the damage assessment, and restart user interaction as needed.",
|
||||||
|
})
|
||||||
|
async reject(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: FileMakerRejectDto,
|
||||||
|
@CurrentUser() fileMaker: any,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.fileMakerRejectV5(
|
||||||
|
fileMaker,
|
||||||
|
claimRequestId,
|
||||||
|
body?.reason,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -404,23 +404,4 @@ export class FileReviewerBlameV5Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Re-submission after FinancialExpert rejection ────────────────────────────
|
|
||||||
|
|
||||||
@Post("resubmit-to-financial-expert/:requestId")
|
|
||||||
@ApiParam({ name: "requestId" })
|
|
||||||
@ApiOperation({
|
|
||||||
summary: "Re-submit rejected file to FinancialExpert (FileReviewer)",
|
|
||||||
description:
|
|
||||||
"Use after the FinancialExpert has rejected the file (FINANCIAL_EXPERT_REJECTED). " +
|
|
||||||
"Moves status back to WAITING_FOR_FINANCIAL_EXPERT for re-review.",
|
|
||||||
})
|
|
||||||
async resubmitToFinancialExpert(
|
|
||||||
@Param("requestId") requestId: string,
|
|
||||||
@CurrentUser() fileReviewer: any,
|
|
||||||
) {
|
|
||||||
return this.requestManagementService.fileReviewerResubmitToFinancialExpert(
|
|
||||||
fileReviewer,
|
|
||||||
requestId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
import {
|
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Param,
|
|
||||||
Post,
|
|
||||||
UseGuards,
|
|
||||||
} from "@nestjs/common";
|
|
||||||
import {
|
|
||||||
ApiBearerAuth,
|
|
||||||
ApiBody,
|
|
||||||
ApiOperation,
|
|
||||||
ApiParam,
|
|
||||||
ApiTags,
|
|
||||||
} from "@nestjs/swagger";
|
|
||||||
import { IsOptional, IsString } from "class-validator";
|
|
||||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
|
||||||
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
|
||||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
|
||||||
import { Roles } from "src/decorators/roles.decorator";
|
|
||||||
import { CurrentUser } from "src/decorators/user.decorator";
|
|
||||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
|
||||||
import { RequestManagementService } from "./request-management.service";
|
|
||||||
|
|
||||||
class FinancialExpertRejectDto {
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description: "Reason for rejection (shown to FileReviewer)",
|
|
||||||
example: "Documents are incomplete or incorrect",
|
|
||||||
})
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
reason?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* V5 FinancialExpert panel.
|
|
||||||
*
|
|
||||||
* The FinancialExpert is the final gatekeeper before a V5 blame file is forwarded
|
|
||||||
* to fanavaran for completion. After the FileReviewer uploads the blame accident
|
|
||||||
* video the file lands in WAITING_FOR_FINANCIAL_EXPERT. The FinancialExpert can:
|
|
||||||
*
|
|
||||||
* approve → WAITING_FOR_EXPERT (file proceeds to damage-expert / fanavaran)
|
|
||||||
* reject → FINANCIAL_EXPERT_REJECTED (FileReviewer must fix and re-submit)
|
|
||||||
*
|
|
||||||
* Re-submission by FileReviewer is handled via the V5 FileReviewer controller
|
|
||||||
* (POST resubmit-to-financial-expert/:requestId).
|
|
||||||
*/
|
|
||||||
@ApiTags("v5 FinancialExpert — blame file financial approval")
|
|
||||||
@Controller("v5/financial-expert/blame-request-management")
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
|
||||||
@Roles(RoleEnum.FINANCIAL_EXPERT)
|
|
||||||
export class FinancialExpertBlameV5Controller {
|
|
||||||
constructor(
|
|
||||||
private readonly requestManagementService: RequestManagementService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get("claim-id/:requestId")
|
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
|
||||||
@ApiOperation({
|
|
||||||
summary: "Get linked claim ID",
|
|
||||||
description:
|
|
||||||
"Returns the claim linked to this V5 blame file. Useful for reviewing claim details before approving.",
|
|
||||||
})
|
|
||||||
async getLinkedClaimId(
|
|
||||||
@Param("requestId") requestId: string,
|
|
||||||
@CurrentUser() financialExpert: any,
|
|
||||||
) {
|
|
||||||
return this.requestManagementService.getV3LinkedClaimForExpert(
|
|
||||||
financialExpert,
|
|
||||||
requestId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post("approve/:requestId")
|
|
||||||
@ApiParam({ name: "requestId" })
|
|
||||||
@ApiOperation({
|
|
||||||
summary: "Approve blame file (FinancialExpert)",
|
|
||||||
description:
|
|
||||||
"Moves the file from WAITING_FOR_FINANCIAL_EXPERT to WAITING_FOR_EXPERT, " +
|
|
||||||
"enabling fanavaran submission by the damage expert.",
|
|
||||||
})
|
|
||||||
async approve(
|
|
||||||
@Param("requestId") requestId: string,
|
|
||||||
@CurrentUser() financialExpert: any,
|
|
||||||
) {
|
|
||||||
return this.requestManagementService.financialExpertApprove(
|
|
||||||
financialExpert,
|
|
||||||
requestId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post("reject/:requestId")
|
|
||||||
@ApiParam({ name: "requestId" })
|
|
||||||
@ApiBody({ type: FinancialExpertRejectDto })
|
|
||||||
@ApiOperation({
|
|
||||||
summary: "Reject blame file (FinancialExpert)",
|
|
||||||
description:
|
|
||||||
"Moves the file from WAITING_FOR_FINANCIAL_EXPERT to FINANCIAL_EXPERT_REJECTED. " +
|
|
||||||
"The FileReviewer must correct the file and call resubmit-to-financial-expert.",
|
|
||||||
})
|
|
||||||
async reject(
|
|
||||||
@Param("requestId") requestId: string,
|
|
||||||
@Body() body: FinancialExpertRejectDto,
|
|
||||||
@CurrentUser() financialExpert: any,
|
|
||||||
) {
|
|
||||||
return this.requestManagementService.financialExpertReject(
|
|
||||||
financialExpert,
|
|
||||||
requestId,
|
|
||||||
body?.reason,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -47,7 +47,7 @@ import { FileMakerBlameV4Controller } from "./file-maker-blame-v4.controller";
|
|||||||
import { FileReviewerBlameV4Controller } from "./file-reviewer-blame-v4.controller";
|
import { FileReviewerBlameV4Controller } from "./file-reviewer-blame-v4.controller";
|
||||||
import { FileMakerBlameV5Controller } from "./file-maker-blame-v5.controller";
|
import { FileMakerBlameV5Controller } from "./file-maker-blame-v5.controller";
|
||||||
import { FileReviewerBlameV5Controller } from "./file-reviewer-blame-v5.controller";
|
import { FileReviewerBlameV5Controller } from "./file-reviewer-blame-v5.controller";
|
||||||
import { FinancialExpertBlameV5Controller } from "./financial-expert-blame-v5.controller";
|
import { FileMakerClaimApprovalV5Controller } from "./file-maker-claim-approval-v5.controller";
|
||||||
import { InquiryRefreshController } from "./inquiry-refresh.controller";
|
import { InquiryRefreshController } from "./inquiry-refresh.controller";
|
||||||
import { InquiryRefreshService } from "./inquiry-refresh.service";
|
import { InquiryRefreshService } from "./inquiry-refresh.service";
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ import { InquiryRefreshService } from "./inquiry-refresh.service";
|
|||||||
FileReviewerBlameV4Controller,
|
FileReviewerBlameV4Controller,
|
||||||
FileMakerBlameV5Controller,
|
FileMakerBlameV5Controller,
|
||||||
FileReviewerBlameV5Controller,
|
FileReviewerBlameV5Controller,
|
||||||
FinancialExpertBlameV5Controller,
|
FileMakerClaimApprovalV5Controller,
|
||||||
InquiryRefreshController,
|
InquiryRefreshController,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -4458,7 +4458,7 @@ export class RequestManagementService {
|
|||||||
/**
|
/**
|
||||||
* Verify the current user (field expert) can access this BlameRequest (v2).
|
* Verify the current user (field expert) can access this BlameRequest (v2).
|
||||||
*/
|
*/
|
||||||
private verifyExpertAccessForBlameV2(req: any, expert: any): void {
|
private async verifyExpertAccessForBlameV2(req: any, expert: any): Promise<void> {
|
||||||
// FILE_REVIEWER can access V4/V5 FileMaker-sealed files.
|
// FILE_REVIEWER can access V4/V5 FileMaker-sealed files.
|
||||||
// They must be the assigned reviewer (or the file is still open for taking).
|
// They must be the assigned reviewer (or the file is still open for taking).
|
||||||
if (expert?.role === RoleEnum.FILE_REVIEWER) {
|
if (expert?.role === RoleEnum.FILE_REVIEWER) {
|
||||||
@@ -4470,15 +4470,18 @@ export class RequestManagementService {
|
|||||||
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 &&
|
||||||
req.status !== CaseStatus.WAITING_FOR_FINANCIAL_EXPERT &&
|
|
||||||
req.status !== CaseStatus.FINANCIAL_EXPERT_REJECTED &&
|
|
||||||
req.status !== CaseStatus.COMPLETED
|
req.status !== CaseStatus.COMPLETED
|
||||||
) {
|
) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
"This file has not been sealed by a FileMaker yet.",
|
"This file has not been sealed by a FileMaker yet.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Enforce reviewer assignment: must be assigned to them or open (no reviewer yet)
|
// Enforce reviewer assignment: must be assigned to them or open (no reviewer yet).
|
||||||
|
// If the file is open (no assignedFileReviewerId) we atomically claim it now —
|
||||||
|
// this handles the case where a reviewer skipped the explicit lock step and went
|
||||||
|
// straight to working on the file (e.g. accident-fields before calling the lock
|
||||||
|
// endpoint). Without this, assignedFileReviewerId is never written and the file
|
||||||
|
// disappears from the reviewer's list after the blame moves to WAITING_FOR_EXPERT.
|
||||||
const assignedId = req.assignedFileReviewerId
|
const assignedId = req.assignedFileReviewerId
|
||||||
? String(req.assignedFileReviewerId)
|
? String(req.assignedFileReviewerId)
|
||||||
: null;
|
: null;
|
||||||
@@ -4487,23 +4490,19 @@ export class RequestManagementService {
|
|||||||
"This file has been taken by another FileReviewer.",
|
"This file has been taken by another FileReviewer.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
if (!assignedId) {
|
||||||
}
|
// Atomically claim — ignore if another reviewer won the race (they would have
|
||||||
// FINANCIAL_EXPERT can read V5 FileMaker-sealed files from the same insurer.
|
// been caught by the assignedId check above on their own first call).
|
||||||
if (expert?.role === RoleEnum.FINANCIAL_EXPERT) {
|
await this.blameRequestDbService.findOneAndUpdate(
|
||||||
if (!req?.isMadeByFileMaker || !req?.expertInitiated || req?.creationMethod !== CreationMethod.IN_PERSON) {
|
{
|
||||||
throw new ForbiddenException(
|
_id: req._id,
|
||||||
"FinancialExpert can only access V5 FileMaker files.",
|
$or: [
|
||||||
);
|
{ assignedFileReviewerId: { $exists: false } },
|
||||||
}
|
{ assignedFileReviewerId: null },
|
||||||
// Scope to same insurer client
|
],
|
||||||
if (
|
},
|
||||||
req.clientKey &&
|
{ $set: { assignedFileReviewerId: new Types.ObjectId(String(expert.sub)) } },
|
||||||
expert.clientKey &&
|
{ new: false },
|
||||||
String(req.clientKey) !== String(expert.clientKey)
|
|
||||||
) {
|
|
||||||
throw new ForbiddenException(
|
|
||||||
"You do not have access to files from a different insurer.",
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -4854,7 +4853,7 @@ export class RequestManagementService {
|
|||||||
"This endpoint is only for expert-initiated LINK blame files.",
|
"This endpoint is only for expert-initiated LINK blame files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
const phone = (dto?.phoneNumber || "").trim();
|
const phone = (dto?.phoneNumber || "").trim();
|
||||||
if (!phone) throw new BadRequestException("phoneNumber is required");
|
if (!phone) throw new BadRequestException("phoneNumber is required");
|
||||||
@@ -4937,7 +4936,7 @@ export class RequestManagementService {
|
|||||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
if (
|
if (
|
||||||
req.type === BlameRequestType.THIRD_PARTY &&
|
req.type === BlameRequestType.THIRD_PARTY &&
|
||||||
!dto.secondPartyPhoneNumber
|
!dto.secondPartyPhoneNumber
|
||||||
@@ -5007,7 +5006,7 @@ export class RequestManagementService {
|
|||||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const verifyOne = async (
|
const verifyOne = async (
|
||||||
@@ -5134,7 +5133,7 @@ export class RequestManagementService {
|
|||||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, actor);
|
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||||
const phone = (dto?.phoneNumber || "").trim();
|
const phone = (dto?.phoneNumber || "").trim();
|
||||||
if (!phone) throw new BadRequestException("phoneNumber is required");
|
if (!phone) throw new BadRequestException("phoneNumber is required");
|
||||||
|
|
||||||
@@ -5220,7 +5219,7 @@ export class RequestManagementService {
|
|||||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, actor);
|
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||||
|
|
||||||
const phone = (dto?.phoneNumber || "").trim();
|
const phone = (dto?.phoneNumber || "").trim();
|
||||||
const otp = (dto?.otp || "").trim();
|
const otp = (dto?.otp || "").trim();
|
||||||
@@ -5991,7 +5990,7 @@ export class RequestManagementService {
|
|||||||
"This endpoint is only for expert-initiated IN_PERSON BlameRequest files.",
|
"This endpoint is only for expert-initiated IN_PERSON BlameRequest files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
if (req.type === BlameRequestType.THIRD_PARTY) {
|
if (req.type === BlameRequestType.THIRD_PARTY) {
|
||||||
return this.expertCompleteThirdPartyFormV2(expert, requestId, formData);
|
return this.expertCompleteThirdPartyFormV2(expert, requestId, formData);
|
||||||
@@ -6025,7 +6024,7 @@ export class RequestManagementService {
|
|||||||
if (req.type !== BlameRequestType.CAR_BODY) {
|
if (req.type !== BlameRequestType.CAR_BODY) {
|
||||||
throw new BadRequestException("File type is not CAR_BODY.");
|
throw new BadRequestException("File type is not CAR_BODY.");
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
if (!formData.carBodyForm) {
|
if (!formData.carBodyForm) {
|
||||||
throw new BadRequestException("carBodyForm is required.");
|
throw new BadRequestException("carBodyForm is required.");
|
||||||
@@ -6230,7 +6229,7 @@ export class RequestManagementService {
|
|||||||
if (req.type !== BlameRequestType.THIRD_PARTY) {
|
if (req.type !== BlameRequestType.THIRD_PARTY) {
|
||||||
throw new BadRequestException("File type is not THIRD_PARTY.");
|
throw new BadRequestException("File type is not THIRD_PARTY.");
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
if (!formData.secondParty || !formData.guiltyPartyPhoneNumber) {
|
if (!formData.secondParty || !formData.guiltyPartyPhoneNumber) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
@@ -6434,7 +6433,7 @@ export class RequestManagementService {
|
|||||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||||
if (firstIdx === -1) throw new BadRequestException("First party not found");
|
if (firstIdx === -1) throw new BadRequestException("First party not found");
|
||||||
@@ -6499,7 +6498,7 @@ export class RequestManagementService {
|
|||||||
"This endpoint is only for expert-initiated files.",
|
"This endpoint is only for expert-initiated files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST);
|
const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST);
|
||||||
if (firstPartyIndex === -1)
|
if (firstPartyIndex === -1)
|
||||||
@@ -6552,7 +6551,7 @@ export class RequestManagementService {
|
|||||||
"This endpoint is only for expert-initiated files.",
|
"This endpoint is only for expert-initiated files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST);
|
const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST);
|
||||||
if (firstPartyIndex === -1)
|
if (firstPartyIndex === -1)
|
||||||
@@ -6617,7 +6616,7 @@ export class RequestManagementService {
|
|||||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
if (req.status !== CaseStatus.WAITING_FOR_SIGNATURES) {
|
if (req.status !== CaseStatus.WAITING_FOR_SIGNATURES) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Request is not waiting for signatures. Current status: " + req.status,
|
"Request is not waiting for signatures. Current status: " + req.status,
|
||||||
@@ -6733,7 +6732,7 @@ export class RequestManagementService {
|
|||||||
"This endpoint is only for expert- or registrar-initiated files.",
|
"This endpoint is only for expert- or registrar-initiated files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
if (!req.expert) req.expert = {} as any;
|
if (!req.expert) req.expert = {} as any;
|
||||||
if (!req.expert.decision) req.expert.decision = {} as any;
|
if (!req.expert.decision) req.expert.decision = {} as any;
|
||||||
@@ -8371,7 +8370,7 @@ export class RequestManagementService {
|
|||||||
const req = await this.blameRequestDbService.findById(blameRequestId);
|
const req = await this.blameRequestDbService.findById(blameRequestId);
|
||||||
if (!req) throw new NotFoundException("Blame request not found");
|
if (!req) throw new NotFoundException("Blame request not found");
|
||||||
this.assertBlameV3ExpertInPerson(req);
|
this.assertBlameV3ExpertInPerson(req);
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
const claim = await this.claimCaseDbService.findOne({
|
const claim = await this.claimCaseDbService.findOne({
|
||||||
blameRequestId: (req as any)._id,
|
blameRequestId: (req as any)._id,
|
||||||
@@ -8952,7 +8951,7 @@ export class RequestManagementService {
|
|||||||
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");
|
||||||
this.assertBlameV3ExpertInPerson(req);
|
this.assertBlameV3ExpertInPerson(req);
|
||||||
this.verifyExpertAccessForBlameV2(req, actor);
|
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||||
|
|
||||||
const partyRole = this.resolveV3InquiryPartyRole(req);
|
const partyRole = this.resolveV3InquiryPartyRole(req);
|
||||||
if (!partyRole) {
|
if (!partyRole) {
|
||||||
@@ -9125,7 +9124,7 @@ export class RequestManagementService {
|
|||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const req = await this.blameRequestDbService.findById(requestId);
|
||||||
if (!req) throw new NotFoundException("Request not found");
|
if (!req) throw new NotFoundException("Request not found");
|
||||||
this.assertBlameV3ExpertInPerson(req);
|
this.assertBlameV3ExpertInPerson(req);
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
if (partyRole === PartyRole.FIRST) {
|
if (partyRole === PartyRole.FIRST) {
|
||||||
if (!this.hasPartyInquiriesComplete(req, PartyRole.FIRST)) {
|
if (!this.hasPartyInquiriesComplete(req, PartyRole.FIRST)) {
|
||||||
@@ -9263,7 +9262,7 @@ export class RequestManagementService {
|
|||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const req = await this.blameRequestDbService.findById(requestId);
|
||||||
if (!req) throw new NotFoundException("Request not found");
|
if (!req) throw new NotFoundException("Request not found");
|
||||||
this.assertBlameV3ExpertInPerson(req);
|
this.assertBlameV3ExpertInPerson(req);
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
this.assertBlameV3AccidentFieldsPhase(req);
|
this.assertBlameV3AccidentFieldsPhase(req);
|
||||||
|
|
||||||
if (!req.expert) req.expert = {} as any;
|
if (!req.expert) req.expert = {} as any;
|
||||||
@@ -9333,7 +9332,7 @@ export class RequestManagementService {
|
|||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const req = await this.blameRequestDbService.findById(requestId);
|
||||||
if (!req) throw new NotFoundException("Request not found");
|
if (!req) throw new NotFoundException("Request not found");
|
||||||
this.assertBlameV3ExpertInPerson(req);
|
this.assertBlameV3ExpertInPerson(req);
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
if (!(req.expert?.decision as any)?.fields?.accidentWay) {
|
if (!(req.expert?.decision as any)?.fields?.accidentWay) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
@@ -9517,7 +9516,7 @@ export class RequestManagementService {
|
|||||||
if (!voice) throw new BadRequestException("File is required");
|
if (!voice) throw new BadRequestException("File is required");
|
||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const req = await this.blameRequestDbService.findById(requestId);
|
||||||
if (!req) throw new NotFoundException("Request not found");
|
if (!req) throw new NotFoundException("Request not found");
|
||||||
this.verifyExpertAccessForBlameV2(req, actor);
|
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||||
const role = this.resolvePartyRoleV3(req, partyRole);
|
const role = this.resolvePartyRoleV3(req, partyRole);
|
||||||
this.assertBlameV3PartyDetailPhase(req, role);
|
this.assertBlameV3PartyDetailPhase(req, role);
|
||||||
const idx = this.getPartyIndex(req, role);
|
const idx = this.getPartyIndex(req, role);
|
||||||
@@ -9558,7 +9557,7 @@ export class RequestManagementService {
|
|||||||
) {
|
) {
|
||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const req = await this.blameRequestDbService.findById(requestId);
|
||||||
if (!req) throw new NotFoundException("Request not found");
|
if (!req) throw new NotFoundException("Request not found");
|
||||||
this.verifyExpertAccessForBlameV2(req, actor);
|
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||||
const role = this.resolvePartyRoleV3(req, partyRole);
|
const role = this.resolvePartyRoleV3(req, partyRole);
|
||||||
this.assertBlameV3PartyDetailPhase(req, role);
|
this.assertBlameV3PartyDetailPhase(req, role);
|
||||||
|
|
||||||
@@ -9590,7 +9589,7 @@ export class RequestManagementService {
|
|||||||
) {
|
) {
|
||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const req = await this.blameRequestDbService.findById(requestId);
|
||||||
if (!req) throw new NotFoundException("Request not found");
|
if (!req) throw new NotFoundException("Request not found");
|
||||||
this.verifyExpertAccessForBlameV2(req, actor);
|
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||||
const role = this.resolvePartyRoleV3(req, partyRole);
|
const role = this.resolvePartyRoleV3(req, partyRole);
|
||||||
this.assertBlameV3PartyDetailPhase(req, role);
|
this.assertBlameV3PartyDetailPhase(req, role);
|
||||||
|
|
||||||
@@ -9628,7 +9627,7 @@ export class RequestManagementService {
|
|||||||
"CAR_BODY accident type is only for CAR_BODY files.",
|
"CAR_BODY accident type is only for CAR_BODY files.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.verifyExpertAccessForBlameV2(req, actor);
|
await this.verifyExpertAccessForBlameV2(req, actor);
|
||||||
this.assertBlameV3ExpertInPerson(req);
|
this.assertBlameV3ExpertInPerson(req);
|
||||||
|
|
||||||
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||||
@@ -9670,8 +9669,10 @@ export class RequestManagementService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* V5 variant of expertUploadBlameVideoV3.
|
* V5 variant of expertUploadBlameVideoV3.
|
||||||
* Identical logic but sets blame status to WAITING_FOR_FINANCIAL_EXPERT instead
|
* Same flow as V3/V4 but additionally marks the linked claim with
|
||||||
* of WAITING_FOR_EXPERT so that a FinancialExpert must approve before fanavaran.
|
* `requiresFileMakerApproval: true` so that after the owner signs,
|
||||||
|
* the claim is held at WAITING_FOR_FILE_MAKER_APPROVAL rather than
|
||||||
|
* being auto-submitted to fanavaran.
|
||||||
*/
|
*/
|
||||||
async expertUploadBlameVideoV5(
|
async expertUploadBlameVideoV5(
|
||||||
expert: any,
|
expert: any,
|
||||||
@@ -9689,7 +9690,7 @@ export class RequestManagementService {
|
|||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const req = await this.blameRequestDbService.findById(requestId);
|
||||||
if (!req) throw new NotFoundException("Request not found");
|
if (!req) throw new NotFoundException("Request not found");
|
||||||
this.assertBlameV3ExpertInPerson(req);
|
this.assertBlameV3ExpertInPerson(req);
|
||||||
this.verifyExpertAccessForBlameV2(req, expert);
|
await this.verifyExpertAccessForBlameV2(req, expert);
|
||||||
|
|
||||||
if (!(req.expert?.decision as any)?.fields?.accidentWay) {
|
if (!(req.expert?.decision as any)?.fields?.accidentWay) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
@@ -9751,7 +9752,7 @@ export class RequestManagementService {
|
|||||||
]);
|
]);
|
||||||
req.workflow.currentStep = WorkflowStep.WAITING_FOR_GUILT_DECISION;
|
req.workflow.currentStep = WorkflowStep.WAITING_FOR_GUILT_DECISION;
|
||||||
req.workflow.nextStep = WorkflowStep.WAITING_FOR_SIGNATURES;
|
req.workflow.nextStep = WorkflowStep.WAITING_FOR_SIGNATURES;
|
||||||
req.status = CaseStatus.WAITING_FOR_FINANCIAL_EXPERT;
|
req.status = CaseStatus.WAITING_FOR_EXPERT;
|
||||||
|
|
||||||
if (!Array.isArray(req.history)) req.history = [];
|
if (!Array.isArray(req.history)) req.history = [];
|
||||||
req.history.push({
|
req.history.push({
|
||||||
@@ -9769,206 +9770,196 @@ export class RequestManagementService {
|
|||||||
}
|
}
|
||||||
await (req as any).save();
|
await (req as any).save();
|
||||||
|
|
||||||
|
// Advance the claim into the damage-expert queue AND mark FileMaker approval
|
||||||
|
// required before fanavaran submission.
|
||||||
|
const fileMakerActorId = req.initiatedByFieldExpertId ?? null;
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(String(claim._id), {
|
||||||
|
$set: {
|
||||||
|
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
||||||
|
claimStatus: ClaimStatus.PENDING,
|
||||||
|
"workflow.currentStep": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||||
|
"workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||||
|
requiresFileMakerApproval: true,
|
||||||
|
...(fileMakerActorId
|
||||||
|
? { fileMakerApprovalActorId: new Types.ObjectId(String(fileMakerActorId)) }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
$push: {
|
||||||
|
"workflow.completedSteps": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||||
|
history: {
|
||||||
|
type: "V5_BLAME_ACCIDENT_VIDEO_UPLOADED",
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(expert.sub),
|
||||||
|
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||||
|
actorType: "file_reviewer",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||||
|
description:
|
||||||
|
"V5 field workflow complete. Claim ready for damage expert review (FileMaker approval required before fanavaran).",
|
||||||
|
v5InPersonFlow: true,
|
||||||
|
fileMakerActorId: fileMakerActorId ? String(fileMakerActorId) : null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requestId: String(req._id),
|
requestId: String(req._id),
|
||||||
publicId: req.publicId,
|
publicId: req.publicId,
|
||||||
videoId: firstParty.evidence.videoId,
|
videoId: firstParty.evidence.videoId,
|
||||||
status: req.status,
|
status: req.status,
|
||||||
message:
|
message:
|
||||||
"Blame accident video uploaded. File is now waiting for FinancialExpert approval.",
|
"Blame accident video uploaded. File is now in expert review queue. " +
|
||||||
|
"After the full claim flow completes and the owner signs, the FileMaker must approve before fanavaran submission.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V5 FinancialExpert approves the file.
|
* V5: FileMaker approves the completed claim.
|
||||||
* Moves blame status from WAITING_FOR_FINANCIAL_EXPERT → WAITING_FOR_EXPERT.
|
*
|
||||||
|
* Preconditions:
|
||||||
|
* - claim.requiresFileMakerApproval === true
|
||||||
|
* - claim.status === WAITING_FOR_FILE_MAKER_APPROVAL
|
||||||
|
* - actor is FILE_MAKER and is the original creator of the linked blame file
|
||||||
|
*
|
||||||
|
* On approval: triggers fanavaran submission and moves claim to COMPLETED.
|
||||||
*/
|
*/
|
||||||
async financialExpertApprove(
|
async fileMakerApproveV5(
|
||||||
financialExpert: any,
|
fileMaker: any,
|
||||||
requestId: string,
|
claimRequestId: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
requestId: string;
|
claimRequestId: string;
|
||||||
publicId: string;
|
publicId: string;
|
||||||
status: string;
|
status: string;
|
||||||
message: string;
|
message: string;
|
||||||
}> {
|
}> {
|
||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
if (!req) throw new NotFoundException("Request not found");
|
if (!claim) throw new NotFoundException("Claim not found");
|
||||||
this.assertBlameV3ExpertInPerson(req);
|
|
||||||
this.verifyFinancialExpertAccess(req, financialExpert);
|
|
||||||
|
|
||||||
if (req.status !== CaseStatus.WAITING_FOR_FINANCIAL_EXPERT) {
|
this.assertFileMakerApprovalAccess(claim, fileMaker);
|
||||||
|
|
||||||
|
if (claim.status !== ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`File must be in WAITING_FOR_FINANCIAL_EXPERT status to approve. Current: ${req.status}`,
|
`Claim must be in WAITING_FOR_FILE_MAKER_APPROVAL status to approve. Current: ${claim.status}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.status = CaseStatus.WAITING_FOR_EXPERT;
|
const actorName = `${fileMaker.firstName || ""} ${fileMaker.lastName || ""}`.trim();
|
||||||
|
const historyEntry = {
|
||||||
if (!Array.isArray(req.history)) req.history = [];
|
type: "V5_FILE_MAKER_APPROVED",
|
||||||
req.history.push({
|
|
||||||
type: "V5_FINANCIAL_EXPERT_APPROVED",
|
|
||||||
actor: {
|
actor: {
|
||||||
actorId: new Types.ObjectId(financialExpert.sub),
|
actorId: new Types.ObjectId(fileMaker.sub),
|
||||||
actorName: `${financialExpert.firstName || ""} ${financialExpert.lastName || ""}`.trim(),
|
actorName,
|
||||||
actorType: "financial_expert",
|
actorType: "file_maker",
|
||||||
},
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
metadata: {},
|
metadata: {},
|
||||||
} as any);
|
};
|
||||||
|
|
||||||
await this.claimCaseDbService.findOneAndUpdate(
|
// Move claim to COMPLETED and clear the approval gate
|
||||||
{ blameRequestId: (req as any)._id },
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
{
|
$set: {
|
||||||
$set: {
|
status: ClaimCaseStatus.COMPLETED,
|
||||||
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
claimStatus: ClaimStatus.APPROVED,
|
||||||
claimStatus: ClaimStatus.PENDING,
|
requiresFileMakerApproval: false,
|
||||||
"workflow.currentStep": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
"workflow.currentStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||||
"workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
"workflow.nextStep": ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||||
},
|
|
||||||
$push: {
|
|
||||||
"workflow.completedSteps": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
|
||||||
history: {
|
|
||||||
type: "STEP_COMPLETED",
|
|
||||||
actor: {
|
|
||||||
actorId: new Types.ObjectId(financialExpert.sub),
|
|
||||||
actorName: `${financialExpert.firstName || ""} ${financialExpert.lastName || ""}`.trim(),
|
|
||||||
actorType: "financial_expert",
|
|
||||||
},
|
|
||||||
timestamp: new Date(),
|
|
||||||
metadata: {
|
|
||||||
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
|
||||||
description:
|
|
||||||
"V5 flow: FinancialExpert approved. Claim ready for damage expert review.",
|
|
||||||
v5Flow: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
$push: {
|
||||||
|
"workflow.completedSteps": ClaimWorkflowStep.INSURER_REVIEW,
|
||||||
await (req as any).save();
|
history: historyEntry,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requestId: String(req._id),
|
claimRequestId,
|
||||||
publicId: req.publicId,
|
publicId: claim.publicId,
|
||||||
status: req.status,
|
status: ClaimCaseStatus.COMPLETED,
|
||||||
message: "File approved by FinancialExpert. Now waiting for expert review.",
|
message:
|
||||||
|
"Claim approved by FileMaker. Claim is now marked COMPLETED. " +
|
||||||
|
"Proceed with fanavaran submission via the claim service.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V5 FinancialExpert rejects the file back to the FileReviewer.
|
* V5: FileMaker rejects the completed claim back to expert review.
|
||||||
* Moves blame status from WAITING_FOR_FINANCIAL_EXPERT → FINANCIAL_EXPERT_REJECTED.
|
*
|
||||||
|
* Moves claim from WAITING_FOR_FILE_MAKER_APPROVAL → WAITING_FOR_DAMAGE_EXPERT
|
||||||
|
* so the FileReviewer can re-lock it and adjust the damage assessment or
|
||||||
|
* restart the back-and-forth with the user.
|
||||||
*/
|
*/
|
||||||
async financialExpertReject(
|
async fileMakerRejectV5(
|
||||||
financialExpert: any,
|
fileMaker: any,
|
||||||
requestId: string,
|
claimRequestId: string,
|
||||||
reason?: string,
|
reason?: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
requestId: string;
|
claimRequestId: string;
|
||||||
publicId: string;
|
publicId: string;
|
||||||
status: string;
|
status: string;
|
||||||
message: string;
|
message: string;
|
||||||
}> {
|
}> {
|
||||||
const req = await this.blameRequestDbService.findById(requestId);
|
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
if (!req) throw new NotFoundException("Request not found");
|
if (!claim) throw new NotFoundException("Claim not found");
|
||||||
this.assertBlameV3ExpertInPerson(req);
|
|
||||||
this.verifyFinancialExpertAccess(req, financialExpert);
|
|
||||||
|
|
||||||
if (req.status !== CaseStatus.WAITING_FOR_FINANCIAL_EXPERT) {
|
this.assertFileMakerApprovalAccess(claim, fileMaker);
|
||||||
|
|
||||||
|
if (claim.status !== ClaimCaseStatus.WAITING_FOR_FILE_MAKER_APPROVAL) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`File must be in WAITING_FOR_FINANCIAL_EXPERT status to reject. Current: ${req.status}`,
|
`Claim must be in WAITING_FOR_FILE_MAKER_APPROVAL status to reject. Current: ${claim.status}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.status = CaseStatus.FINANCIAL_EXPERT_REJECTED;
|
const actorName = `${fileMaker.firstName || ""} ${fileMaker.lastName || ""}`.trim();
|
||||||
|
|
||||||
if (!Array.isArray(req.history)) req.history = [];
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
req.history.push({
|
$set: {
|
||||||
type: "V5_FINANCIAL_EXPERT_REJECTED",
|
status: ClaimCaseStatus.FILE_MAKER_REJECTED,
|
||||||
actor: {
|
claimStatus: ClaimStatus.NEEDS_REVISION,
|
||||||
actorId: new Types.ObjectId(financialExpert.sub),
|
"workflow.currentStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||||
actorName: `${financialExpert.firstName || ""} ${financialExpert.lastName || ""}`.trim(),
|
"workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||||
actorType: "financial_expert",
|
|
||||||
},
|
},
|
||||||
metadata: { reason: reason ?? null },
|
$push: {
|
||||||
} as any);
|
history: {
|
||||||
|
type: "V5_FILE_MAKER_REJECTED",
|
||||||
await (req as any).save();
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(fileMaker.sub),
|
||||||
|
actorName,
|
||||||
|
actorType: "file_maker",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: { reason: reason ?? null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requestId: String(req._id),
|
claimRequestId,
|
||||||
publicId: req.publicId,
|
publicId: claim.publicId,
|
||||||
status: req.status,
|
status: ClaimCaseStatus.FILE_MAKER_REJECTED,
|
||||||
message: "File rejected by FinancialExpert. FileReviewer must correct and re-submit.",
|
message:
|
||||||
|
"Claim rejected by FileMaker. FileReviewer can re-lock the claim and adjust the damage assessment or restart the user interaction.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Guard for V5 FileMaker approval endpoints. */
|
||||||
* V5 FileReviewer re-submits a rejected file for FinancialExpert re-review.
|
private assertFileMakerApprovalAccess(claim: any, fileMaker: any): void {
|
||||||
* Moves blame status from FINANCIAL_EXPERT_REJECTED → WAITING_FOR_FINANCIAL_EXPERT.
|
if (fileMaker?.role !== RoleEnum.FILE_MAKER) {
|
||||||
*/
|
throw new ForbiddenException("Only FileMakers can perform this action.");
|
||||||
async fileReviewerResubmitToFinancialExpert(
|
|
||||||
fileReviewer: any,
|
|
||||||
requestId: string,
|
|
||||||
): Promise<{
|
|
||||||
requestId: string;
|
|
||||||
publicId: string;
|
|
||||||
status: string;
|
|
||||||
message: string;
|
|
||||||
}> {
|
|
||||||
const req = await this.blameRequestDbService.findById(requestId);
|
|
||||||
if (!req) throw new NotFoundException("Request not found");
|
|
||||||
this.assertBlameV3ExpertInPerson(req);
|
|
||||||
this.verifyExpertAccessForBlameV2(req, fileReviewer);
|
|
||||||
|
|
||||||
if (req.status !== CaseStatus.FINANCIAL_EXPERT_REJECTED) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
`File must be in FINANCIAL_EXPERT_REJECTED status to re-submit. Current: ${req.status}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
if (!claim.requiresFileMakerApproval) {
|
||||||
req.status = CaseStatus.WAITING_FOR_FINANCIAL_EXPERT;
|
|
||||||
|
|
||||||
if (!Array.isArray(req.history)) req.history = [];
|
|
||||||
req.history.push({
|
|
||||||
type: "V5_FILE_REVIEWER_RESUBMITTED",
|
|
||||||
actor: {
|
|
||||||
actorId: new Types.ObjectId(fileReviewer.sub),
|
|
||||||
actorName: `${fileReviewer.firstName || ""} ${fileReviewer.lastName || ""}`.trim(),
|
|
||||||
actorType: "file_reviewer",
|
|
||||||
},
|
|
||||||
metadata: {},
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
await (req as any).save();
|
|
||||||
|
|
||||||
return {
|
|
||||||
requestId: String(req._id),
|
|
||||||
publicId: req.publicId,
|
|
||||||
status: req.status,
|
|
||||||
message: "File re-submitted to FinancialExpert for approval.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Guard: ensures the actor is a FinancialExpert with access to this V5 file. */
|
|
||||||
private verifyFinancialExpertAccess(req: any, financialExpert: any): void {
|
|
||||||
if (financialExpert?.role !== RoleEnum.FINANCIAL_EXPERT) {
|
|
||||||
throw new ForbiddenException("Only FinancialExperts can perform this action.");
|
|
||||||
}
|
|
||||||
if (!req?.isMadeByFileMaker || !req?.expertInitiated || req?.creationMethod !== CreationMethod.IN_PERSON) {
|
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
"FinancialExpert can only access V5 FileMaker files.",
|
"This claim does not require FileMaker approval (not a V5 file).",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Scope to same insurer client
|
// Scope to the FileMaker who created the linked blame file
|
||||||
if (
|
if (
|
||||||
req.clientKey &&
|
claim.fileMakerApprovalActorId &&
|
||||||
financialExpert.clientKey &&
|
String(claim.fileMakerApprovalActorId) !== String(fileMaker.sub)
|
||||||
String(req.clientKey) !== String(financialExpert.clientKey)
|
|
||||||
) {
|
) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
"You do not have access to files from a different insurer.",
|
"Only the FileMaker who created this file can approve or reject it.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,42 +74,3 @@ export class CreateRegistrarAdminDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
clientId: string;
|
clientId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateFinancialExpertAdminDto {
|
|
||||||
@ApiProperty({ example: "financial@insurer.ir" })
|
|
||||||
@IsEmail()
|
|
||||||
email: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: "FinExpert@724" })
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
password: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: "Ali" })
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
firstName: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: "Mohammadi" })
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
lastName: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: "09121234567" })
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
mobile: string;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
example: "664a1b2c3d4e5f6789012345",
|
|
||||||
description: "Mongo ObjectId of the insurer client this financial expert belongs to.",
|
|
||||||
})
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
clientId: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: "02112345678" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
phone?: string;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
CreateSuperAdminDto,
|
CreateSuperAdminDto,
|
||||||
CreateFieldExpertAdminDto,
|
CreateFieldExpertAdminDto,
|
||||||
CreateRegistrarAdminDto,
|
CreateRegistrarAdminDto,
|
||||||
CreateFinancialExpertAdminDto,
|
|
||||||
} from "./dto/super-admin.dto";
|
} from "./dto/super-admin.dto";
|
||||||
import { ClientDto } from "src/client/dto/create-client.dto";
|
import { ClientDto } from "src/client/dto/create-client.dto";
|
||||||
import {
|
import {
|
||||||
@@ -130,18 +129,6 @@ export class SuperAdminController {
|
|||||||
return this.superAdminService.createRegistrar(body);
|
return this.superAdminService.createRegistrar(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("create-financial-expert")
|
|
||||||
@ApiOperation({
|
|
||||||
summary: "Create a financial expert (V5 flow)",
|
|
||||||
description:
|
|
||||||
"FinancialExperts are the final approvers in the V5 blame flow before fanavaran submission. " +
|
|
||||||
"Scoped to the specified insurer client via clientId.",
|
|
||||||
})
|
|
||||||
@ApiBody({ type: CreateFinancialExpertAdminDto })
|
|
||||||
createFinancialExpert(@Body() body: CreateFinancialExpertAdminDto) {
|
|
||||||
return this.superAdminService.createFinancialExpert(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── System settings ──────────────────────────────────────────────────────
|
// ── System settings ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Get("system-settings")
|
@Get("system-settings")
|
||||||
|
|||||||
@@ -19,12 +19,10 @@ import {
|
|||||||
CreateSuperAdminDto,
|
CreateSuperAdminDto,
|
||||||
CreateFieldExpertAdminDto,
|
CreateFieldExpertAdminDto,
|
||||||
CreateRegistrarAdminDto,
|
CreateRegistrarAdminDto,
|
||||||
CreateFinancialExpertAdminDto,
|
|
||||||
} from "./dto/super-admin.dto";
|
} from "./dto/super-admin.dto";
|
||||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service";
|
import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service";
|
||||||
import { RegistrarDbService } from "src/users/entities/db-service/registrar.db.service";
|
import { RegistrarDbService } from "src/users/entities/db-service/registrar.db.service";
|
||||||
import { FinancialExpertDbService } from "src/users/entities/db-service/financial-expert.db.service";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SuperAdminService {
|
export class SuperAdminService {
|
||||||
@@ -35,7 +33,6 @@ export class SuperAdminService {
|
|||||||
private readonly actorAuthService: ActorAuthService,
|
private readonly actorAuthService: ActorAuthService,
|
||||||
private readonly fieldExpertDbService: FieldExpertDbService,
|
private readonly fieldExpertDbService: FieldExpertDbService,
|
||||||
private readonly registrarDbService: RegistrarDbService,
|
private readonly registrarDbService: RegistrarDbService,
|
||||||
private readonly financialExpertDbService: FinancialExpertDbService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** List all super-admin accounts (sans password). */
|
/** List all super-admin accounts (sans password). */
|
||||||
@@ -102,29 +99,4 @@ export class SuperAdminService {
|
|||||||
async createRegistrar(body: CreateRegistrarAdminDto) {
|
async createRegistrar(body: CreateRegistrarAdminDto) {
|
||||||
return this.actorAuthService.createRegistrarMock(body);
|
return this.actorAuthService.createRegistrarMock(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFinancialExpert(body: CreateFinancialExpertAdminDto) {
|
|
||||||
const email = body.email.toLowerCase().trim();
|
|
||||||
const existing = await this.financialExpertDbService.findOne({ email });
|
|
||||||
if (existing) {
|
|
||||||
throw new ConflictException(
|
|
||||||
"A FinancialExpert with this email already exists.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const hashedPassword = await this.hashService.hash(body.password);
|
|
||||||
const created = await this.financialExpertDbService.create({
|
|
||||||
email,
|
|
||||||
password: hashedPassword,
|
|
||||||
firstName: body.firstName,
|
|
||||||
lastName: body.lastName,
|
|
||||||
mobile: body.mobile,
|
|
||||||
phone: body.phone,
|
|
||||||
clientKey: new Types.ObjectId(body.clientId),
|
|
||||||
role: RoleEnum.FINANCIAL_EXPERT,
|
|
||||||
} as any);
|
|
||||||
const { password: _pw, ...rest } = (created as any).toObject
|
|
||||||
? (created as any).toObject()
|
|
||||||
: (created as any);
|
|
||||||
return rest;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { InjectModel } from "@nestjs/mongoose";
|
|
||||||
import { FilterQuery, Model, Types } from "mongoose";
|
|
||||||
import { FinancialExpertModel } from "../schema/financial-expert.schema";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FinancialExpertDbService {
|
|
||||||
constructor(
|
|
||||||
@InjectModel(FinancialExpertModel.name)
|
|
||||||
private readonly financialExpertModel: Model<FinancialExpertModel>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async create(
|
|
||||||
user: Partial<FinancialExpertModel> & { password: string },
|
|
||||||
): Promise<FinancialExpertModel> {
|
|
||||||
return await this.financialExpertModel.create(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOne(
|
|
||||||
filter: FilterQuery<FinancialExpertModel>,
|
|
||||||
): Promise<FinancialExpertModel | null> {
|
|
||||||
return await this.financialExpertModel.findOne(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findByLoginIdentifier(
|
|
||||||
identifier: string,
|
|
||||||
): Promise<FinancialExpertModel | null> {
|
|
||||||
const id = identifier.trim();
|
|
||||||
const or: FilterQuery<FinancialExpertModel>[] = [
|
|
||||||
{ email: id },
|
|
||||||
{ username: id },
|
|
||||||
];
|
|
||||||
if (/^\d{10}$/.test(id)) {
|
|
||||||
or.push({ nationalCode: id });
|
|
||||||
}
|
|
||||||
return await this.financialExpertModel.findOne({ $or: or });
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(userId: string): Promise<FinancialExpertModel | null> {
|
|
||||||
return await this.financialExpertModel.findOne({
|
|
||||||
_id: new Types.ObjectId(userId),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOneAndUpdate(
|
|
||||||
filter: FilterQuery<FinancialExpertModel>,
|
|
||||||
update: FilterQuery<FinancialExpertModel>,
|
|
||||||
) {
|
|
||||||
return await this.financialExpertModel.findOneAndUpdate(filter, update, {
|
|
||||||
new: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAll(
|
|
||||||
filter: FilterQuery<FinancialExpertModel>,
|
|
||||||
): Promise<(FinancialExpertModel & { _id: Types.ObjectId })[]> {
|
|
||||||
return await this.financialExpertModel.find(filter).lean();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateOne(filter: FilterQuery<FinancialExpertModel>, update: any) {
|
|
||||||
return this.financialExpertModel.updateOne(filter, update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
|
||||||
|
|
||||||
@Schema({
|
|
||||||
collection: "financial-expert",
|
|
||||||
versionKey: false,
|
|
||||||
timestamps: true,
|
|
||||||
})
|
|
||||||
export class FinancialExpertModel {
|
|
||||||
_id?: Types.ObjectId;
|
|
||||||
|
|
||||||
@Prop({ required: true })
|
|
||||||
firstName: string;
|
|
||||||
|
|
||||||
@Prop({ required: true })
|
|
||||||
lastName: string;
|
|
||||||
|
|
||||||
@Prop({ type: String, unique: true, sparse: true, required: false })
|
|
||||||
email?: string;
|
|
||||||
|
|
||||||
@Prop({ type: String })
|
|
||||||
username?: string;
|
|
||||||
|
|
||||||
@Prop({ type: String, index: true, sparse: true })
|
|
||||||
nationalCode?: string;
|
|
||||||
|
|
||||||
@Prop({ type: Types.ObjectId, index: true })
|
|
||||||
clientKey?: Types.ObjectId;
|
|
||||||
|
|
||||||
@Prop({ type: Types.ObjectId, index: true })
|
|
||||||
branchId?: Types.ObjectId;
|
|
||||||
|
|
||||||
@Prop({ required: true })
|
|
||||||
password: string;
|
|
||||||
|
|
||||||
@Prop({ required: false })
|
|
||||||
mobile?: string;
|
|
||||||
|
|
||||||
@Prop({ required: false })
|
|
||||||
phone?: string;
|
|
||||||
|
|
||||||
@Prop({ default: RoleEnum.FINANCIAL_EXPERT })
|
|
||||||
role: RoleEnum;
|
|
||||||
|
|
||||||
@Prop({ type: "string", default: "" })
|
|
||||||
otp: string;
|
|
||||||
|
|
||||||
@Prop({ type: "string", required: false })
|
|
||||||
expertCode?: string;
|
|
||||||
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FinancialExpertDbSchema = SchemaFactory.createForClass(
|
|
||||||
FinancialExpertModel,
|
|
||||||
);
|
|
||||||
|
|
||||||
FinancialExpertDbSchema.index(
|
|
||||||
{ clientKey: 1, nationalCode: 1 },
|
|
||||||
{ unique: true, sparse: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
FinancialExpertDbSchema.pre("save", function (next) {
|
|
||||||
if (!this.username) {
|
|
||||||
this.username = (this as any).email || (this as any).nationalCode;
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
@@ -11,7 +11,6 @@ import { UserDbService } from "./entities/db-service/user.db.service";
|
|||||||
import { ExpertFileActivityDbService } from "./entities/db-service/expert-file-activity.db.service";
|
import { ExpertFileActivityDbService } from "./entities/db-service/expert-file-activity.db.service";
|
||||||
import { FileMakerDbService } from "./entities/db-service/file-maker.db.service";
|
import { FileMakerDbService } from "./entities/db-service/file-maker.db.service";
|
||||||
import { FileReviewerDbService } from "./entities/db-service/file-reviewer.db.service";
|
import { FileReviewerDbService } from "./entities/db-service/file-reviewer.db.service";
|
||||||
import { FinancialExpertDbService } from "./entities/db-service/financial-expert.db.service";
|
|
||||||
import {
|
import {
|
||||||
DamageExpertDbSchema,
|
DamageExpertDbSchema,
|
||||||
DamageExpertModel,
|
DamageExpertModel,
|
||||||
@@ -42,10 +41,6 @@ import {
|
|||||||
FileReviewerDbSchema,
|
FileReviewerDbSchema,
|
||||||
FileReviewerModel,
|
FileReviewerModel,
|
||||||
} from "./entities/schema/file-reviewer.schema";
|
} from "./entities/schema/file-reviewer.schema";
|
||||||
import {
|
|
||||||
FinancialExpertDbSchema,
|
|
||||||
FinancialExpertModel,
|
|
||||||
} from "./entities/schema/financial-expert.schema";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -59,7 +54,6 @@ import {
|
|||||||
{ name: ExpertFileActivity.name, schema: ExpertFileActivitySchema },
|
{ name: ExpertFileActivity.name, schema: ExpertFileActivitySchema },
|
||||||
{ name: FileMakerModel.name, schema: FileMakerDbSchema },
|
{ name: FileMakerModel.name, schema: FileMakerDbSchema },
|
||||||
{ name: FileReviewerModel.name, schema: FileReviewerDbSchema },
|
{ name: FileReviewerModel.name, schema: FileReviewerDbSchema },
|
||||||
{ name: FinancialExpertModel.name, schema: FinancialExpertDbSchema },
|
|
||||||
]),
|
]),
|
||||||
HashModule,
|
HashModule,
|
||||||
],
|
],
|
||||||
@@ -74,7 +68,6 @@ import {
|
|||||||
ExpertFileActivityDbService,
|
ExpertFileActivityDbService,
|
||||||
FileMakerDbService,
|
FileMakerDbService,
|
||||||
FileReviewerDbService,
|
FileReviewerDbService,
|
||||||
FinancialExpertDbService,
|
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
UserDbService,
|
UserDbService,
|
||||||
@@ -86,7 +79,6 @@ import {
|
|||||||
ExpertFileActivityDbService,
|
ExpertFileActivityDbService,
|
||||||
FileMakerDbService,
|
FileMakerDbService,
|
||||||
FileReviewerDbService,
|
FileReviewerDbService,
|
||||||
FinancialExpertDbService,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user