forked from Yara724/api
YARA-725
This commit is contained in:
@@ -46,6 +46,7 @@ import { RequestManagementService } from "src/request-management/request-managem
|
||||
import { snapshotFromFieldExpert } from "src/helpers/expert-profile-snapshot";
|
||||
import { ExpertModel } from "src/users/entities/schema/expert.schema";
|
||||
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||
|
||||
interface CheckedRequestEntry {
|
||||
CheckedRequest?: {
|
||||
@@ -407,6 +408,27 @@ export class ExpertBlameService {
|
||||
};
|
||||
}
|
||||
|
||||
/** True while another expert holds an active workflow lock (uses `expiredAt` when set). */
|
||||
private isBlameV2WorkflowLockCurrentlyEnforced(doc: {
|
||||
workflow?: {
|
||||
locked?: boolean;
|
||||
lockedAt?: Date;
|
||||
expiredAt?: Date;
|
||||
lockedBy?: { actorId?: unknown };
|
||||
};
|
||||
}): boolean {
|
||||
if (!doc?.workflow?.locked) return false;
|
||||
const exp = doc.workflow.expiredAt;
|
||||
if (exp) {
|
||||
return Date.now() < new Date(exp as Date).getTime();
|
||||
}
|
||||
const la = doc.workflow.lockedAt;
|
||||
if (!la) return true;
|
||||
return (
|
||||
Date.now() < new Date(la as Date).getTime() + this.blameV2LockTtlMs
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist unlock when blame V2 workflow lock is older than {@link blameV2LockTtlMs}.
|
||||
* V1 uses in-memory `scheduleUnlock`; V2 only stored `workflow.lockedAt`, so locks never cleared until now.
|
||||
@@ -420,6 +442,13 @@ export class ExpertBlameService {
|
||||
}
|
||||
|
||||
const lockedAt = request.workflow.lockedAt;
|
||||
const expiredAt = request.workflow.expiredAt;
|
||||
if (
|
||||
expiredAt &&
|
||||
Date.now() < new Date(expiredAt as Date).getTime()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
lockedAt &&
|
||||
Date.now() <
|
||||
@@ -433,7 +462,9 @@ export class ExpertBlameService {
|
||||
_id: new Types.ObjectId(requestId),
|
||||
"workflow.locked": true,
|
||||
};
|
||||
if (lockedAt) {
|
||||
if (expiredAt) {
|
||||
filter["workflow.expiredAt"] = expiredAt;
|
||||
} else if (lockedAt) {
|
||||
filter["workflow.lockedAt"] = lockedAt;
|
||||
} else if (lockedById) {
|
||||
filter["workflow.lockedBy.actorId"] = new Types.ObjectId(
|
||||
@@ -715,6 +746,7 @@ export class ExpertBlameService {
|
||||
try {
|
||||
requireActorClientKey(actor);
|
||||
const actorId = actor.sub;
|
||||
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
|
||||
const doc = await this.blameRequestDbService.findByIdWithoutHistory(requestId);
|
||||
if (!doc) {
|
||||
throw new NotFoundException("Request not found");
|
||||
@@ -736,6 +768,16 @@ export class ExpertBlameService {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isBlameV2WorkflowLockCurrentlyEnforced(doc)) {
|
||||
const w = doc.workflow as { lockedBy?: { actorId?: unknown } } | undefined;
|
||||
const lockerId = String(w?.lockedBy?.actorId ?? "");
|
||||
if (lockerId && lockerId !== actorId) {
|
||||
throw new ForbiddenException(
|
||||
"This request is locked by another expert.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const decision = (doc.expert as any)?.decision;
|
||||
const decidedByExpertId = decision?.decidedByExpertId;
|
||||
if (decidedByExpertId && String(decidedByExpertId) !== actorId) {
|
||||
@@ -924,29 +966,22 @@ export class ExpertBlameService {
|
||||
);
|
||||
}
|
||||
|
||||
// Check if locked and not expired
|
||||
if (request.workflow?.locked) {
|
||||
const lockedAt = request.workflow.lockedAt;
|
||||
if (lockedAt) {
|
||||
const lockExpiryTime =
|
||||
new Date(lockedAt).getTime() + this.blameV2LockTtlMs;
|
||||
if (Date.now() < lockExpiryTime) {
|
||||
// Lock is still valid
|
||||
const lockedByActorId = String(
|
||||
request.workflow.lockedBy?.actorId ?? "",
|
||||
);
|
||||
if (lockedByActorId === actorDetail.sub) {
|
||||
throw new BadRequestException(
|
||||
"You have already locked this request",
|
||||
);
|
||||
} else {
|
||||
throw new BadRequestException(
|
||||
"Request is currently locked by another expert",
|
||||
);
|
||||
}
|
||||
}
|
||||
// Lock expired, allow re-locking (continue below)
|
||||
// Check if locked and not expired (`expiredAt` or lockedAt + TTL)
|
||||
if (
|
||||
request.workflow?.locked &&
|
||||
this.isBlameV2WorkflowLockCurrentlyEnforced(request)
|
||||
) {
|
||||
const lockedByActorId = String(
|
||||
request.workflow.lockedBy?.actorId ?? "",
|
||||
);
|
||||
if (lockedByActorId === actorDetail.sub) {
|
||||
throw new BadRequestException(
|
||||
"You have already locked this request",
|
||||
);
|
||||
}
|
||||
throw new BadRequestException(
|
||||
"Request is currently locked by another expert",
|
||||
);
|
||||
}
|
||||
|
||||
const lockSnapshot = await this.snapshotFieldExpert(actorDetail.sub);
|
||||
@@ -988,6 +1023,7 @@ export class ExpertBlameService {
|
||||
await this.smsOrchestrationService.sendThirdPartyExpertStartedReviewNotice(
|
||||
{
|
||||
receptor: phone,
|
||||
fileKind: "blame",
|
||||
publicId: request.publicId,
|
||||
expertLastName,
|
||||
},
|
||||
@@ -1153,30 +1189,48 @@ export class ExpertBlameService {
|
||||
requestId,
|
||||
);
|
||||
|
||||
const requestIdToken = String(request._id);
|
||||
const partyPhoneByUserId = new Map<string, string>();
|
||||
for (const p of request.parties || []) {
|
||||
const uid = p?.person?.userId ? String(p.person.userId) : "";
|
||||
const phone = p?.person?.phoneNumber;
|
||||
if (uid && typeof phone === "string" && phone.length > 0) {
|
||||
partyPhoneByUserId.set(uid, phone);
|
||||
if (request.type === BlameRequestType.THIRD_PARTY) {
|
||||
const requestIdToken = String(request._id);
|
||||
const partyPhoneByUserId = new Map<string, string>();
|
||||
for (const p of request.parties || []) {
|
||||
const uid = p?.person?.userId ? String(p.person.userId) : "";
|
||||
const phone = p?.person?.phoneNumber;
|
||||
if (uid && typeof phone === "string" && phone.length > 0) {
|
||||
partyPhoneByUserId.set(uid, phone);
|
||||
}
|
||||
}
|
||||
const partyHasResendPayload = (row: {
|
||||
requestedItems?: ResendItemType[];
|
||||
description?: string;
|
||||
}) => {
|
||||
const n = Array.isArray(row.requestedItems)
|
||||
? row.requestedItems.length
|
||||
: 0;
|
||||
return n > 0 || String(row.description ?? "").trim().length > 0;
|
||||
};
|
||||
for (const p of partyResendRequests) {
|
||||
if (!partyHasResendPayload(p)) continue;
|
||||
const uid = String((p as any).partyId);
|
||||
const phone = partyPhoneByUserId.get(uid);
|
||||
if (!phone) continue;
|
||||
const role =
|
||||
String(
|
||||
request.parties?.find(
|
||||
(x: any) => String(x?.person?.userId) === uid,
|
||||
)?.role,
|
||||
) === "SECOND"
|
||||
? "SECOND"
|
||||
: "FIRST";
|
||||
await this.smsOrchestrationService.sendResendDocumentsNotice({
|
||||
receptor: phone,
|
||||
fileKind: "blame",
|
||||
publicId: request.publicId,
|
||||
link: this.smsOrchestrationService.buildBlamePartyLink(
|
||||
requestIdToken,
|
||||
role,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const p of partyResendRequests) {
|
||||
const uid = String((p as any).partyId);
|
||||
const phone = partyPhoneByUserId.get(uid);
|
||||
if (!phone) continue;
|
||||
const role =
|
||||
String(request.parties?.find((x: any) => String(x?.person?.userId) === uid)?.role) ===
|
||||
"SECOND"
|
||||
? "SECOND"
|
||||
: "FIRST";
|
||||
await this.smsOrchestrationService.sendResendDocumentsNotice({
|
||||
receptor: phone,
|
||||
fileKind: "blame",
|
||||
publicId: request.publicId,
|
||||
link: this.smsOrchestrationService.buildBlamePartyLink(requestIdToken, role),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1333,14 +1387,17 @@ export class ExpertBlameService {
|
||||
);
|
||||
}
|
||||
|
||||
// THIRD_PARTY: both parties must sign; send each their deep link (mutual-agreement uses yara-blame-agreement elsewhere).
|
||||
if (request.type === BlameRequestType.THIRD_PARTY) {
|
||||
const requestIdToken = String(request._id);
|
||||
const expertLastName =
|
||||
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
|
||||
for (const p of request.parties || []) {
|
||||
const parties = request.parties || [];
|
||||
for (const partyRole of [PartyRole.FIRST, PartyRole.SECOND] as const) {
|
||||
const p = parties.find((x: any) => x?.role === partyRole);
|
||||
const phone = p?.person?.phoneNumber;
|
||||
if (!phone || typeof phone !== "string") continue;
|
||||
const role = String(p?.role) === "SECOND" ? "SECOND" : "FIRST";
|
||||
const linkRole = partyRole === PartyRole.SECOND ? "SECOND" : "FIRST";
|
||||
await this.smsOrchestrationService.sendSignatureReviewNotice({
|
||||
receptor: phone,
|
||||
fileKind: "blame",
|
||||
@@ -1348,7 +1405,7 @@ export class ExpertBlameService {
|
||||
expertLastName,
|
||||
link: this.smsOrchestrationService.buildBlamePartyLink(
|
||||
requestIdToken,
|
||||
role,
|
||||
linkRole,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user