forked from Yara724/api
YARA-725
This commit is contained in:
@@ -29,6 +29,7 @@ import { ReqClaimStatus } from "src/Types&Enums/claim-request-management/status.
|
||||
import { ClaimStepsEnum } from "src/Types&Enums/claim-request-management/steps.enum";
|
||||
import { UserType } from "src/Types&Enums/userType.enum";
|
||||
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
||||
import { UserDbService } from "src/users/entities/db-service/user.db.service";
|
||||
import { ClaimPerIdRs } from "./dto/claim-list-perId-rs.dto";
|
||||
import { ClaimListDtoRs } from "./dto/claim-list-rs.dto";
|
||||
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
|
||||
@@ -189,6 +190,7 @@ export class ExpertClaimService {
|
||||
private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService,
|
||||
private readonly branchDbService: BranchDbService,
|
||||
private readonly smsOrchestrationService: SmsOrchestrationService,
|
||||
private readonly userDbService: UserDbService,
|
||||
) { }
|
||||
|
||||
/** Load immutable profile fields from `damage-expert` for the acting expert. */
|
||||
@@ -197,6 +199,29 @@ export class ExpertClaimService {
|
||||
return snapshotFromDamageExpert(doc as DamageExpertModel);
|
||||
}
|
||||
|
||||
/** Owner mobile: linked blame party phone when available, else `users.mobile`. */
|
||||
private async resolveClaimOwnerPhone(claim: any): Promise<string | undefined> {
|
||||
if (!claim?.owner?.userId) return undefined;
|
||||
const ownerUserId = String(claim.owner.userId);
|
||||
if (claim.blameRequestId) {
|
||||
const blame = await this.blameRequestDbService.findById(
|
||||
claim.blameRequestId.toString(),
|
||||
);
|
||||
const ownerParty = (blame?.parties || []).find(
|
||||
(p: any) =>
|
||||
p?.person?.userId && String(p.person.userId) === ownerUserId,
|
||||
);
|
||||
const fromParty = ownerParty?.person?.phoneNumber;
|
||||
if (fromParty && typeof fromParty === "string") return fromParty;
|
||||
}
|
||||
const user = await this.userDbService.findOne({
|
||||
_id: new Types.ObjectId(ownerUserId),
|
||||
});
|
||||
const m = user?.mobile;
|
||||
if (m && typeof m === "string") return m;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Map blame party `Vehicle` (name/model/type) to expert panel shape. */
|
||||
private expertVehicleFromPartyVehicle(v: any): {
|
||||
carName?: string;
|
||||
@@ -2009,6 +2034,19 @@ export class ExpertClaimService {
|
||||
},
|
||||
});
|
||||
|
||||
const ownerPhoneFactors = await this.resolveClaimOwnerPhone(claim);
|
||||
if (ownerPhoneFactors) {
|
||||
const expertLastName =
|
||||
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
|
||||
await this.smsOrchestrationService.sendSignatureReviewNotice({
|
||||
receptor: ownerPhoneFactors,
|
||||
fileKind: "claim",
|
||||
publicId: claim.publicId,
|
||||
expertLastName,
|
||||
link: this.smsOrchestrationService.buildClaimLink(String(claim._id)),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
message:
|
||||
"All factors approved. The claim proceeds to insurer review (same as non-factor expert reply).",
|
||||
@@ -2072,6 +2110,7 @@ export class ExpertClaimService {
|
||||
*/
|
||||
async lockClaimRequestV2(claimRequestId: string, actor: any) {
|
||||
requireActorClientKey(actor);
|
||||
await this.expireClaimWorkflowLockV2IfStale(claimRequestId);
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claim) {
|
||||
@@ -2131,6 +2170,20 @@ export class ExpertClaimService {
|
||||
},
|
||||
});
|
||||
|
||||
const ownerPhone = await this.resolveClaimOwnerPhone(claim);
|
||||
if (ownerPhone) {
|
||||
const expertLastName =
|
||||
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
|
||||
await this.smsOrchestrationService.sendThirdPartyExpertStartedReviewNotice(
|
||||
{
|
||||
receptor: ownerPhone,
|
||||
fileKind: "claim",
|
||||
publicId: claim.publicId,
|
||||
expertLastName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
claimRequestId,
|
||||
locked: true,
|
||||
@@ -2229,28 +2282,14 @@ export class ExpertClaimService {
|
||||
},
|
||||
});
|
||||
|
||||
if (claim.blameRequestId) {
|
||||
const blame = await this.blameRequestDbService.findById(
|
||||
claim.blameRequestId.toString(),
|
||||
);
|
||||
if (blame?.type === BlameRequestType.THIRD_PARTY) {
|
||||
const ownerUserId = claim.owner?.userId ? String(claim.owner.userId) : "";
|
||||
const ownerParty = (blame.parties || []).find(
|
||||
(p: any) =>
|
||||
p?.person?.userId && String(p.person.userId) === ownerUserId,
|
||||
);
|
||||
const ownerPhone = ownerParty?.person?.phoneNumber;
|
||||
if (ownerPhone && typeof ownerPhone === "string") {
|
||||
await this.smsOrchestrationService.sendResendDocumentsNotice({
|
||||
receptor: ownerPhone,
|
||||
fileKind: "claim",
|
||||
publicId: claim.publicId,
|
||||
link: this.smsOrchestrationService.buildClaimLink(
|
||||
String(claim._id),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
const ownerPhoneResend = await this.resolveClaimOwnerPhone(claim);
|
||||
if (ownerPhoneResend) {
|
||||
await this.smsOrchestrationService.sendResendDocumentsNotice({
|
||||
receptor: ownerPhoneResend,
|
||||
fileKind: "claim",
|
||||
publicId: claim.publicId,
|
||||
link: this.smsOrchestrationService.buildClaimLink(String(claim._id)),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -2410,30 +2449,21 @@ export class ExpertClaimService {
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, updatePayload);
|
||||
|
||||
if (!needsFactorUpload && claim.blameRequestId) {
|
||||
const blame = await this.blameRequestDbService.findById(
|
||||
claim.blameRequestId.toString(),
|
||||
);
|
||||
if (blame?.type === BlameRequestType.THIRD_PARTY) {
|
||||
const ownerUserId = claim.owner?.userId ? String(claim.owner.userId) : "";
|
||||
const ownerParty = (blame.parties || []).find(
|
||||
(p: any) =>
|
||||
p?.person?.userId && String(p.person.userId) === ownerUserId,
|
||||
);
|
||||
const ownerPhone = ownerParty?.person?.phoneNumber;
|
||||
if (ownerPhone && typeof ownerPhone === "string") {
|
||||
const expertLastName =
|
||||
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
|
||||
await this.smsOrchestrationService.sendSignatureReviewNotice({
|
||||
receptor: ownerPhone,
|
||||
fileKind: "claim",
|
||||
publicId: claim.publicId,
|
||||
expertLastName,
|
||||
link: this.smsOrchestrationService.buildClaimLink(
|
||||
String(claim._id),
|
||||
),
|
||||
});
|
||||
}
|
||||
// Same effective step as WAITING_FOR_INSURER_APPROVAL @ INSURER_REVIEW: owner reviews/signs (yara-signature).
|
||||
if (!needsFactorUpload) {
|
||||
const ownerPhone = await this.resolveClaimOwnerPhone(claim);
|
||||
if (ownerPhone) {
|
||||
const expertLastName =
|
||||
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
|
||||
await this.smsOrchestrationService.sendSignatureReviewNotice({
|
||||
receptor: ownerPhone,
|
||||
fileKind: "claim",
|
||||
publicId: claim.publicId,
|
||||
expertLastName,
|
||||
link: this.smsOrchestrationService.buildClaimLink(
|
||||
String(claim._id),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2579,6 +2609,15 @@ export class ExpertClaimService {
|
||||
claimCaseTouchesClient(c, clientKey),
|
||||
);
|
||||
|
||||
for (const c of filtered) {
|
||||
if (
|
||||
c.workflow?.locked &&
|
||||
!this.isClaimV2WorkflowLockCurrentlyEnforced(c)
|
||||
) {
|
||||
await this.expireClaimWorkflowLockV2IfStale(String(c._id));
|
||||
}
|
||||
}
|
||||
|
||||
const blameIds = [
|
||||
...new Set(
|
||||
filtered
|
||||
@@ -2607,18 +2646,23 @@ export class ExpertClaimService {
|
||||
? blameById.get(c.blameRequestId.toString())
|
||||
: undefined;
|
||||
const fileCtx = blame ? this.blameFileContextForExpert(blame) : {};
|
||||
const lockActive =
|
||||
!!(c.workflow?.locked && this.isClaimV2WorkflowLockCurrentlyEnforced(c));
|
||||
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,
|
||||
locked: lockActive,
|
||||
lockedBy:
|
||||
lockActive && c.workflow?.lockedBy
|
||||
? {
|
||||
actorId: c.workflow.lockedBy.actorId?.toString(),
|
||||
actorName: c.workflow.lockedBy.actorName,
|
||||
lockedAt: (c.workflow as any).lockedAt?.toISOString?.(),
|
||||
expiredAt: (c.workflow as any).expiredAt?.toISOString?.(),
|
||||
}
|
||||
: undefined,
|
||||
vehicle: v
|
||||
? {
|
||||
carName: v.carName,
|
||||
@@ -2749,11 +2793,15 @@ export class ExpertClaimService {
|
||||
}
|
||||
|
||||
/**
|
||||
* True when another expert still holds an active (non-expired) workflow lock.
|
||||
* Missing `lockedAt` on old documents: treat lock as still enforced until cleared.
|
||||
* True while a workflow lock is still within its window (`expiredAt` when set, else lockedAt + TTL).
|
||||
* Missing both timestamps on old documents: treat lock as enforced until cleared.
|
||||
*/
|
||||
private isClaimV2WorkflowLockCurrentlyEnforced(claim: any): boolean {
|
||||
if (!claim?.workflow?.locked) return false;
|
||||
const exp = claim.workflow?.expiredAt;
|
||||
if (exp) {
|
||||
return Date.now() < new Date(exp as Date).getTime();
|
||||
}
|
||||
const la = claim.workflow?.lockedAt as Date | string | undefined;
|
||||
if (!la) return true;
|
||||
return (
|
||||
@@ -2762,6 +2810,55 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
/** Persist unlock when claim V2 workflow lock TTL has passed (matches blame V2 behaviour). */
|
||||
private async expireClaimWorkflowLockV2IfStale(
|
||||
claimRequestId: string,
|
||||
): Promise<void> {
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!claim?.workflow?.locked) return;
|
||||
|
||||
const expiredAt = claim.workflow?.expiredAt;
|
||||
const lockedAt = claim.workflow?.lockedAt;
|
||||
const now = Date.now();
|
||||
const ttl = this.claimV2WorkflowLockTtlMs;
|
||||
|
||||
if (expiredAt && now < new Date(expiredAt as Date).getTime()) return;
|
||||
if (
|
||||
!expiredAt &&
|
||||
lockedAt &&
|
||||
now < new Date(lockedAt as Date).getTime() + ttl
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!expiredAt && !lockedAt) return;
|
||||
|
||||
const lockedById = claim.workflow.lockedBy?.actorId;
|
||||
const filter: Record<string, unknown> = {
|
||||
_id: new Types.ObjectId(claimRequestId),
|
||||
"workflow.locked": true,
|
||||
};
|
||||
if (expiredAt) {
|
||||
filter["workflow.expiredAt"] = expiredAt;
|
||||
} else if (lockedAt) {
|
||||
filter["workflow.lockedAt"] = lockedAt;
|
||||
} else if (lockedById) {
|
||||
filter["workflow.lockedBy.actorId"] = lockedById;
|
||||
}
|
||||
|
||||
await this.claimCaseDbService.findOneAndUpdate(
|
||||
filter as any,
|
||||
{
|
||||
$set: { "workflow.locked": false },
|
||||
$unset: {
|
||||
"workflow.lockedAt": "",
|
||||
"workflow.expiredAt": "",
|
||||
"workflow.lockedBy": "",
|
||||
},
|
||||
},
|
||||
{ new: false },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Get claim detail for damage expert
|
||||
*
|
||||
@@ -2776,6 +2873,7 @@ export class ExpertClaimService {
|
||||
actor: any,
|
||||
): Promise<ClaimDetailV2ResponseDto> {
|
||||
const actorId = actor.sub;
|
||||
await this.expireClaimWorkflowLockV2IfStale(claimRequestId);
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
if (!claim) {
|
||||
|
||||
Reference in New Issue
Block a user