1
0
forked from Yara724/api

Merge remote-tracking branch 'upstream/main' | Merge upstream into main and reapply local changes

This commit is contained in:
2026-04-18 12:30:32 +03:30
19 changed files with 1317 additions and 1323 deletions

View File

@@ -33,6 +33,11 @@ import { ClaimListDtoRs } from "./dto/claim-list-rs.dto";
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
import {
assertClaimCaseForTenant,
claimCaseTouchesClient,
requireActorClientKey,
} from "src/helpers/tenant-scope";
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
import { GetClaimListV2ResponseDto, ClaimListItemV2Dto } from "./dto/claim-list-v2.dto";
import { ClaimDetailV2ResponseDto } from "./dto/claim-detail-v2.dto";
@@ -319,8 +324,18 @@ export class ExpertClaimService {
await this.claimRequestManagementDbService.findOne(requestId);
if (!request) throw new BadRequestException("Claim request not found");
if (request.lockFile) {
throw new BadRequestException("Claim request is locked");
const isLocked = !!request.lockFile;
const lockedByCurrent =
String(request?.actorLocked?.actorId || "") === String(actorDetail.sub);
const isLockExpired =
!!request.unlockTime && Date.now() >= new Date(request.unlockTime).getTime();
// Idempotent behavior: same expert can re-enter their locked file.
if (isLocked && lockedByCurrent && !isLockExpired) {
return { _id: requestId, lock: true, message: "Already locked by you" };
}
if (isLocked && !lockedByCurrent && !isLockExpired) {
throw new BadRequestException("Claim request is locked by another expert");
}
if (request.claimStatus === ReqClaimStatus.UserPending) {
@@ -1602,13 +1617,15 @@ export class ExpertClaimService {
async validateClaimFactorsV2(
claimRequestId: string,
body: FactorValidationV2Dto,
actor: { sub: string; fullName?: string },
actor: { sub: string; fullName?: string; clientKey?: string },
) {
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim) {
throw new NotFoundException("Claim request not found");
}
assertClaimCaseForTenant(claim, actor);
if (
claim.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL ||
claim.claimStatus !== ClaimStatus.UNDER_REVIEW ||
@@ -1842,12 +1859,15 @@ export class ExpertClaimService {
* - Sets currentStep = EXPERT_DAMAGE_ASSESSMENT
*/
async lockClaimRequestV2(claimRequestId: string, actor: any) {
requireActorClientKey(actor);
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim) {
throw new NotFoundException('Claim request not found');
}
assertClaimCaseForTenant(claim, actor);
if (claim.status !== ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT) {
throw new BadRequestException(
`Claim is not available for locking. Current status: ${claim.status}`,
@@ -1913,6 +1933,8 @@ export class ExpertClaimService {
throw new NotFoundException('Claim request not found');
}
assertClaimCaseForTenant(claim, actor);
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
throw new BadRequestException(
`Claim is not in EXPERT_REVIEWING state. Current status: ${claim.status}`,
@@ -1994,12 +2016,15 @@ export class ExpertClaimService {
reply: import('./dto/expert-claim-v2.dto').SubmitExpertReplyV2Dto,
actor: any,
) {
requireActorClientKey(actor);
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim) {
throw new NotFoundException('Claim request not found');
}
assertClaimCaseForTenant(claim, actor);
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
throw new BadRequestException(
`Claim is not in a reviewable state. Current status: ${claim.status}`,
@@ -2131,12 +2156,15 @@ export class ExpertClaimService {
* - Unlocks the workflow so user can act
*/
async requestInPersonVisitV2(claimRequestId: string, actor: any, note?: string) {
requireActorClientKey(actor);
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim) {
throw new NotFoundException('Claim request not found');
}
assertClaimCaseForTenant(claim, actor);
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
throw new BadRequestException(
`Claim is not in EXPERT_REVIEWING state. Current status: ${claim.status}`,
@@ -2179,12 +2207,15 @@ export class ExpertClaimService {
/**
* V2: Get claim list for damage expert
*
* Returns claims that are:
* 1. WAITING_FOR_DAMAGE_EXPERT status AND not locked (available for anyone to pick)
* 2. WAITING_FOR_DAMAGE_EXPERT status AND not locked (claimStatus PENDING)
* 3. Any status locked by THIS expert (their own in-progress work)
* Returns claims that are (then filtered to the actor's insurer via `claimCaseTouchesClient`):
* 1. WAITING_FOR_DAMAGE_EXPERT and not locked (open queue)
* 2. Locked by this expert (in-progress)
* 3. WAITING_FOR_INSURER_APPROVAL + UNDER_REVIEW at EXPERT_COST_EVALUATION (factor validation queue)
*/
async getClaimListV2(actorId: string): Promise<GetClaimListV2ResponseDto> {
async getClaimListV2(actor: any): Promise<GetClaimListV2ResponseDto> {
requireActorClientKey(actor);
const actorId = actor.sub;
const clientKey = actor.clientKey as string;
const claims = await this.claimCaseDbService.find({
$or: [
// Available claims: waiting for expert, not locked
@@ -2206,34 +2237,36 @@ export class ExpertClaimService {
],
});
const list = (claims as any[]).map((c) => {
const awaitingFactorValidation =
c.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL &&
c.claimStatus === ClaimStatus.UNDER_REVIEW &&
c.workflow?.currentStep === ClaimWorkflowStep.EXPERT_COST_EVALUATION;
return {
claimRequestId: c._id.toString(),
publicId: c.publicId,
status: c.status,
currentStep: c.workflow?.currentStep || '',
locked: c.workflow?.locked || false,
lockedBy: c.workflow?.lockedBy
? {
actorId: c.workflow.lockedBy.actorId?.toString(),
actorName: c.workflow.lockedBy.actorName,
}
: undefined,
vehicle: c.vehicle
? {
carName: c.vehicle.carName,
carModel: c.vehicle.carModel,
carType: c.vehicle.carType,
}
: undefined,
createdAt: c.createdAt,
awaitingFactorValidation,
};
}) as ClaimListItemV2Dto[];
const list = (claims as any[])
.filter((c) => claimCaseTouchesClient(c, clientKey))
.map((c) => {
const awaitingFactorValidation =
c.status === ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL &&
c.claimStatus === ClaimStatus.UNDER_REVIEW &&
c.workflow?.currentStep === ClaimWorkflowStep.EXPERT_COST_EVALUATION;
return {
claimRequestId: c._id.toString(),
publicId: c.publicId,
status: c.status,
currentStep: c.workflow?.currentStep || '',
locked: c.workflow?.locked || false,
lockedBy: c.workflow?.lockedBy
? {
actorId: c.workflow.lockedBy.actorId?.toString(),
actorName: c.workflow.lockedBy.actorName,
}
: undefined,
vehicle: c.vehicle
? {
carName: c.vehicle.carName,
carModel: c.vehicle.carModel,
carType: c.vehicle.carType,
}
: undefined,
createdAt: c.createdAt,
awaitingFactorValidation,
};
}) as ClaimListItemV2Dto[];
return { list, total: list.length };
}
@@ -2242,20 +2275,23 @@ export class ExpertClaimService {
* V2: Get claim detail for damage expert
*
* Validations:
* - Claim must exist
* - Claim status must be WAITING_FOR_DAMAGE_EXPERT
* - If claim is locked, only the locking expert can access it
* - Claim must exist and belong to the actor's insurer (`assertClaimCaseForTenant`)
* - Allowed when picking up/reviewing: WAITING_FOR_DAMAGE_EXPERT (if locked, only the locking expert)
* - Or when validating factors: WAITING_FOR_INSURER_APPROVAL + UNDER_REVIEW at EXPERT_COST_EVALUATION
*/
async getClaimDetailV2(
claimRequestId: string,
actorId: string,
actor: any,
): Promise<ClaimDetailV2ResponseDto> {
const actorId = actor.sub;
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim) {
throw new NotFoundException('Claim request not found');
}
assertClaimCaseForTenant(claim, actor);
const isPickupReview =
claim.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT;
const isFactorValidationPending =
@@ -2377,10 +2413,12 @@ export class ExpertClaimService {
body: UpdateClaimDamagedPartsV2Dto,
actor: any,
) {
requireActorClientKey(actor);
const claim = await this.claimCaseDbService.findById(claimRequestId);
if (!claim) {
throw new NotFoundException("Claim request not found");
}
assertClaimCaseForTenant(claim, actor);
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
throw new BadRequestException(
`Claim is not in EXPERT_REVIEWING state. Current status: ${claim.status}`,