forked from Yara724/api
Fixed lock mechanism, and some resendItem types
This commit is contained in:
@@ -3,4 +3,6 @@ export enum InPersonDocumentsEnum {
|
||||
CarCertificate = "carCertificate",
|
||||
DrivingLicense = "drivingLicense",
|
||||
CarGreenCard = "carGreenCard",
|
||||
}
|
||||
Plate = "plate",
|
||||
CarPlate = "carPlate",
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Types } from "mongoose";
|
||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
||||
import {
|
||||
ExpertProfileSnapshot,
|
||||
ExpertProfileSnapshotSchema,
|
||||
@@ -23,6 +24,22 @@ export class ClaimActorLock {
|
||||
}
|
||||
export const ClaimActorLockSchema = SchemaFactory.createForClass(ClaimActorLock);
|
||||
|
||||
/** Captures queue fields before a damage-expert lock; restored if the lock TTL expires without a reply. */
|
||||
@Schema({ _id: false })
|
||||
export class ClaimPreLockQueueSnapshot {
|
||||
@Prop({ type: String, enum: ClaimStatus, required: true })
|
||||
claimStatus: ClaimStatus;
|
||||
|
||||
@Prop({ type: String, enum: ClaimWorkflowStep, required: true })
|
||||
currentStep: ClaimWorkflowStep;
|
||||
|
||||
@Prop({ type: String, enum: ClaimWorkflowStep, required: false })
|
||||
nextStep?: ClaimWorkflowStep;
|
||||
}
|
||||
export const ClaimPreLockQueueSnapshotSchema = SchemaFactory.createForClass(
|
||||
ClaimPreLockQueueSnapshot,
|
||||
);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimWorkflow {
|
||||
@Prop({ type: String, enum: ClaimWorkflowStep })
|
||||
@@ -46,6 +63,9 @@ export class ClaimWorkflow {
|
||||
|
||||
@Prop({ type: ClaimActorLockSchema })
|
||||
lockedBy?: ClaimActorLock;
|
||||
|
||||
@Prop({ type: ClaimPreLockQueueSnapshotSchema })
|
||||
preLockQueueSnapshot?: ClaimPreLockQueueSnapshot;
|
||||
}
|
||||
export const ClaimWorkflowSchema = SchemaFactory.createForClass(ClaimWorkflow);
|
||||
|
||||
|
||||
@@ -305,15 +305,7 @@ export class ExpertBlameService {
|
||||
for (const doc of visibleCases) {
|
||||
const w = doc.workflow as Record<string, unknown> | undefined;
|
||||
if (!w?.locked) continue;
|
||||
const la = w.lockedAt;
|
||||
if (!la) {
|
||||
staleIds.add(String(doc._id));
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
Date.now() >=
|
||||
new Date(la as string | Date).getTime() + this.blameV2LockTtlMs
|
||||
) {
|
||||
if (!this.isBlameV2WorkflowLockCurrentlyEnforced(doc as any)) {
|
||||
staleIds.add(String(doc._id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2158,6 +2158,14 @@ export class ExpertClaimService {
|
||||
actorRole: 'damage_expert',
|
||||
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||
},
|
||||
'workflow.preLockQueueSnapshot': {
|
||||
claimStatus: claim.claimStatus,
|
||||
currentStep:
|
||||
claim.workflow?.currentStep ?? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
...(claim.workflow?.nextStep != null
|
||||
? { nextStep: claim.workflow.nextStep }
|
||||
: {}),
|
||||
},
|
||||
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||
$push: {
|
||||
history: {
|
||||
@@ -2264,6 +2272,7 @@ export class ExpertClaimService {
|
||||
"workflow.lockedAt": "",
|
||||
"workflow.expiredAt": "",
|
||||
"workflow.lockedBy": "",
|
||||
"workflow.preLockQueueSnapshot": "",
|
||||
},
|
||||
$push: {
|
||||
history: {
|
||||
@@ -2419,6 +2428,7 @@ export class ExpertClaimService {
|
||||
'workflow.lockedAt': '',
|
||||
'workflow.expiredAt': '',
|
||||
'workflow.lockedBy': '',
|
||||
'workflow.preLockQueueSnapshot': '',
|
||||
},
|
||||
'workflow.currentStep': nextStep,
|
||||
'workflow.nextStep': needsFactorUpload
|
||||
@@ -2529,6 +2539,7 @@ export class ExpertClaimService {
|
||||
'workflow.lockedAt': '',
|
||||
'workflow.expiredAt': '',
|
||||
'workflow.lockedBy': '',
|
||||
'workflow.preLockQueueSnapshot': '',
|
||||
},
|
||||
...(note ? { 'evaluation.visitLocation': note } : {}),
|
||||
...(visitSnapshot && {
|
||||
@@ -2584,6 +2595,8 @@ export class ExpertClaimService {
|
||||
requireActorClientKey(actor);
|
||||
const actorId = actor.sub;
|
||||
const clientKey = actor.clientKey as string;
|
||||
const lockTtlMs = this.claimV2WorkflowLockTtlMs;
|
||||
const now = new Date();
|
||||
const claims = await this.claimCaseDbService.find({
|
||||
$or: [
|
||||
// Available claims: waiting for expert, not locked
|
||||
@@ -2596,6 +2609,27 @@ export class ExpertClaimService {
|
||||
'workflow.locked': true,
|
||||
'workflow.lockedBy.actorId': new Types.ObjectId(actorId),
|
||||
},
|
||||
// EXPERT_REVIEWING but lock cleared or TTL elapsed (reconcile on read; fixes invisible rows)
|
||||
{
|
||||
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
||||
$or: [
|
||||
{ 'workflow.locked': { $ne: true } },
|
||||
{ 'workflow.expiredAt': { $lte: now } },
|
||||
{
|
||||
$expr: {
|
||||
$and: [
|
||||
{ $eq: ['$workflow.locked', true] },
|
||||
{
|
||||
$lte: [
|
||||
{ $add: ['$workflow.lockedAt', lockTtlMs] },
|
||||
now,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// User uploaded all factors; expert must approve/reject (unlocked queue)
|
||||
{
|
||||
status: ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
|
||||
@@ -2609,12 +2643,32 @@ export class ExpertClaimService {
|
||||
claimCaseTouchesClient(c, clientKey),
|
||||
);
|
||||
|
||||
for (const c of filtered) {
|
||||
if (
|
||||
const staleLockToReconcile = filtered.filter(
|
||||
(c) =>
|
||||
c.workflow?.locked &&
|
||||
!this.isClaimV2WorkflowLockCurrentlyEnforced(c)
|
||||
) {
|
||||
await this.expireClaimWorkflowLockV2IfStale(String(c._id));
|
||||
!this.isClaimV2WorkflowLockCurrentlyEnforced(c),
|
||||
);
|
||||
const touchedIds = (
|
||||
await Promise.all(
|
||||
staleLockToReconcile.map(async (c) => {
|
||||
const updated = await this.expireClaimWorkflowLockV2IfStale(
|
||||
String(c._id),
|
||||
);
|
||||
return updated ? String(c._id) : null;
|
||||
}),
|
||||
)
|
||||
).filter((id): id is string => id != null);
|
||||
|
||||
if (touchedIds.length > 0) {
|
||||
const refreshed = await this.claimCaseDbService.find({
|
||||
_id: { $in: touchedIds.map((id) => new Types.ObjectId(id)) },
|
||||
});
|
||||
const byId = new Map(
|
||||
refreshed.map((r) => [String(r._id), r] as const),
|
||||
);
|
||||
for (const c of filtered) {
|
||||
const r = byId.get(String(c._id));
|
||||
if (r) Object.assign(c, r);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2792,6 +2846,18 @@ export class ExpertClaimService {
|
||||
};
|
||||
}
|
||||
|
||||
private claimHasDamageExpertReply(claim: {
|
||||
evaluation?: {
|
||||
damageExpertReply?: unknown;
|
||||
damageExpertReplyFinal?: unknown;
|
||||
};
|
||||
}): boolean {
|
||||
return !!(
|
||||
claim.evaluation?.damageExpertReply ||
|
||||
claim.evaluation?.damageExpertReplyFinal
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -2810,27 +2876,31 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
/** Persist unlock when claim V2 workflow lock TTL has passed (matches blame V2 behaviour). */
|
||||
/**
|
||||
* Persist unlock when claim V2 workflow lock TTL has passed.
|
||||
* If the expert never submitted a damage reply, restores queue fields from
|
||||
* `workflow.preLockQueueSnapshot` (or defaults) so the case shows as WAITING_FOR_DAMAGE_EXPERT again.
|
||||
*/
|
||||
private async expireClaimWorkflowLockV2IfStale(
|
||||
claimRequestId: string,
|
||||
): Promise<void> {
|
||||
): Promise<boolean> {
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!claim?.workflow?.locked) return;
|
||||
if (!claim?.workflow?.locked) return false;
|
||||
|
||||
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 && now < new Date(expiredAt as Date).getTime()) return false;
|
||||
if (
|
||||
!expiredAt &&
|
||||
lockedAt &&
|
||||
now < new Date(lockedAt as Date).getTime() + ttl
|
||||
) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (!expiredAt && !lockedAt) return;
|
||||
if (!expiredAt && !lockedAt) return false;
|
||||
|
||||
const lockedById = claim.workflow.lockedBy?.actorId;
|
||||
const filter: Record<string, unknown> = {
|
||||
@@ -2845,18 +2915,55 @@ export class ExpertClaimService {
|
||||
filter["workflow.lockedBy.actorId"] = lockedById;
|
||||
}
|
||||
|
||||
await this.claimCaseDbService.findOneAndUpdate(
|
||||
const needsQueueRestore =
|
||||
claim.status === ClaimCaseStatus.EXPERT_REVIEWING &&
|
||||
!this.claimHasDamageExpertReply(claim);
|
||||
|
||||
const snap = claim.workflow?.preLockQueueSnapshot as
|
||||
| {
|
||||
claimStatus?: ClaimStatus;
|
||||
currentStep?: ClaimWorkflowStep;
|
||||
nextStep?: ClaimWorkflowStep;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
const restoreClaimStatus = snap?.claimStatus ?? ClaimStatus.PENDING;
|
||||
const restoreCurrent =
|
||||
snap?.currentStep ?? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
||||
const restoreNext =
|
||||
snap?.nextStep != null
|
||||
? snap.nextStep
|
||||
: ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
||||
|
||||
const lockFieldsUnset = {
|
||||
"workflow.lockedAt": "",
|
||||
"workflow.expiredAt": "",
|
||||
"workflow.lockedBy": "",
|
||||
"workflow.preLockQueueSnapshot": "",
|
||||
};
|
||||
|
||||
const update = needsQueueRestore
|
||||
? {
|
||||
$set: {
|
||||
"workflow.locked": false,
|
||||
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
||||
claimStatus: restoreClaimStatus,
|
||||
"workflow.currentStep": restoreCurrent,
|
||||
"workflow.nextStep": restoreNext,
|
||||
},
|
||||
$unset: lockFieldsUnset,
|
||||
}
|
||||
: {
|
||||
$set: { "workflow.locked": false },
|
||||
$unset: lockFieldsUnset,
|
||||
};
|
||||
|
||||
const cleared = await this.claimCaseDbService.findOneAndUpdate(
|
||||
filter as any,
|
||||
{
|
||||
$set: { "workflow.locked": false },
|
||||
$unset: {
|
||||
"workflow.lockedAt": "",
|
||||
"workflow.expiredAt": "",
|
||||
"workflow.lockedBy": "",
|
||||
},
|
||||
},
|
||||
update as any,
|
||||
{ new: false },
|
||||
);
|
||||
return !!cleared;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6446,6 +6446,29 @@ export class RequestManagementService {
|
||||
"workflow.completedSteps": "WAITING_FOR_SIGNATURES",
|
||||
},
|
||||
});
|
||||
|
||||
if (updatedRequest.type === BlameRequestType.THIRD_PARTY) {
|
||||
const guiltyPartyId = updatedRequest.expert?.decision?.guiltyPartyId
|
||||
? String(updatedRequest.expert.decision.guiltyPartyId)
|
||||
: null;
|
||||
if (guiltyPartyId) {
|
||||
const damagedParty = updatedRequest.parties?.find(
|
||||
(p) => p.person?.userId && String(p.person.userId) !== guiltyPartyId,
|
||||
);
|
||||
const phone = damagedParty?.person?.phoneNumber?.trim();
|
||||
if (phone) {
|
||||
const publicId = String(updatedRequest.publicId);
|
||||
const link = this.smsOrchestrationService.buildClaimLink(requestId);
|
||||
await this.smsOrchestrationService.sendThirdPartyDamagedPartyClaimLinkNotice(
|
||||
{
|
||||
receptor: phone,
|
||||
publicId,
|
||||
link,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
finalStatus = CaseStatus.STOPPED;
|
||||
message =
|
||||
|
||||
@@ -117,6 +117,20 @@ export class SmsOrchestrationService implements OnModuleInit {
|
||||
});
|
||||
}
|
||||
|
||||
/** THIRD_PARTY only: notify the damaged (non-guilty) party that blame is completed and they can open the claim flow. */
|
||||
async sendThirdPartyDamagedPartyClaimLinkNotice(params: {
|
||||
receptor: string;
|
||||
publicId: string;
|
||||
link: string;
|
||||
}): Promise<boolean> {
|
||||
return this.sendTemplate({
|
||||
template: "yara-claim-link",
|
||||
receptor: params.receptor,
|
||||
token: params.publicId,
|
||||
token2: params.link,
|
||||
});
|
||||
}
|
||||
|
||||
async sendThirdPartyExpertStartedReviewNotice(
|
||||
params: {
|
||||
receptor: string;
|
||||
|
||||
Reference in New Issue
Block a user