Fixed legacy requestedCounts methods and statistics + claimLink address

This commit is contained in:
SepehrYahyaee
2026-04-27 15:29:44 +03:30
parent 362e02ddc4
commit c2f996cc28
17 changed files with 441 additions and 394 deletions

View File

@@ -47,6 +47,11 @@ 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";
import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service";
import {
ExpertFileActivityType,
ExpertFileKind,
} from "src/users/entities/schema/expert-file-activity.schema";
interface CheckedRequestEntry {
CheckedRequest?: {
@@ -88,6 +93,7 @@ export class ExpertBlameService {
private readonly userSignDbService: UserSignDbService,
private readonly requestManagementService: RequestManagementService,
private readonly smsOrchestrationService: SmsOrchestrationService,
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
) { }
/** Load immutable profile fields from `expert` for the acting field expert. */
@@ -482,21 +488,22 @@ export class ExpertBlameService {
}
if (!request.expert?.decision && lockedById) {
const rid = new Types.ObjectId(requestId).toString();
try {
await this.expertDbService.findOneAndUpdate(
{ _id: new Types.ObjectId(String(lockedById)) },
{
$inc: { "requestStats.totalChecked": -1 },
$pull: { countedRequests: rid } as any,
},
);
await this.recordBlameExpertActivity({
expertId: String(lockedById),
requestId: String(requestId),
eventType: ExpertFileActivityType.UNCHECKED,
tenantId:
String(request.parties?.[0]?.person?.clientId ?? "") ||
String(request.parties?.[1]?.person?.clientId ?? ""),
idempotencyKey: `blame:${requestId}:unchecked:expired:${lockedById}`,
});
this.logger.warn(
`Blame case ${requestId} workflow lock expired; unlocked in DB and rolled back checked stat for expert ${lockedById}`,
`Blame case ${requestId} workflow lock expired; unlocked in DB and emitted unchecked activity for expert ${lockedById}`,
);
} catch (e) {
this.logger.warn(
`Expert stat rollback failed after blame lock expiry ${requestId}`,
`Expert activity emit failed after blame lock expiry ${requestId}`,
e,
);
}
@@ -521,13 +528,15 @@ export class ExpertBlameService {
if (shouldDecrementChecked) {
updateExp.blameStatus = ReqBlameStatus.UnChecked;
await this.expertDbService.findOneAndUpdate(
{ _id: new Types.ObjectId(r.actorLocked.actorId) },
{
$inc: { "requestStats.totalChecked": -1 },
$pull: { countedRequests: r._id.toString() },
},
);
await this.recordBlameExpertActivity({
expertId: String(r.actorLocked.actorId),
requestId: String(r._id),
eventType: ExpertFileActivityType.UNCHECKED,
tenantId:
String(r.firstPartyDetails?.firstPartyClient?.clientId ?? "") ||
String(r.secondPartyDetails?.secondPartyClient?.clientId ?? ""),
idempotencyKey: `blame:${String(r._id)}:unchecked:unlock:${String(r.actorLocked.actorId)}`,
});
this.logger.warn(
`Request ${r._id} unlocked without reply — expert stats rolled back.`,
@@ -577,13 +586,15 @@ export class ExpertBlameService {
if (shouldRollbackStats) {
update.blameStatus = ReqBlameStatus.UnChecked;
await this.expertDbService.findOneAndUpdate(
{ _id: new Types.ObjectId(current.actorLocked.actorId) },
{
$inc: { "requestStats.totalChecked": -1 },
$pull: { countedRequests: current._id.toString() },
},
);
await this.recordBlameExpertActivity({
expertId: String(current.actorLocked.actorId),
requestId: String(current._id),
eventType: ExpertFileActivityType.UNCHECKED,
tenantId:
String(current.firstPartyDetails?.firstPartyClient?.clientId ?? "") ||
String(current.secondPartyDetails?.secondPartyClient?.clientId ?? ""),
idempotencyKey: `blame:${String(current._id)}:unchecked:auto-unlock:${String(current.actorLocked.actorId)}`,
});
this.logger.warn(
`Request ${current._id} auto-unlocked (no reply) — expert stats rolled back.`,
@@ -912,8 +923,16 @@ export class ExpertBlameService {
throw new BadRequestException("Request already locked or invalid status");
}
// Update expert stats atomically (use findOneAndUpdate with conditions)
await this.updateDamageExpertStats(actorDetail.sub, requestId, "checked");
// Record expert check activity
await this.recordBlameExpertActivity({
expertId: String(actorDetail.sub),
requestId: String(requestId),
eventType: ExpertFileActivityType.CHECKED,
tenantId:
String(updateResult?.firstPartyDetails?.firstPartyClient?.clientId ?? "") ||
String(updateResult?.secondPartyDetails?.secondPartyClient?.clientId ?? ""),
idempotencyKey: `blame:${requestId}:checked:${actorDetail.sub}`,
});
this.scheduleUnlock(updateResult);
return { _id: requestId, lock: true };
@@ -1002,8 +1021,15 @@ export class ExpertBlameService {
throw new InternalServerErrorException("Failed to lock the request");
}
// Update expert stats (reusing existing helper)
await this.updateDamageExpertStats(actorDetail.sub, requestId, "checked");
await this.recordBlameExpertActivity({
expertId: String(actorDetail.sub),
requestId: String(requestId),
eventType: ExpertFileActivityType.CHECKED,
tenantId:
String(request.parties?.[0]?.person?.clientId ?? "") ||
String(request.parties?.[1]?.person?.clientId ?? ""),
idempotencyKey: `blame:${requestId}:checked:${actorDetail.sub}`,
});
if (request.type === BlameRequestType.THIRD_PARTY) {
const phones = (request.parties || [])
@@ -1177,6 +1203,16 @@ export class ExpertBlameService {
);
}
await this.recordBlameExpertActivity({
expertId: String(actorId),
requestId: String(requestId),
eventType: ExpertFileActivityType.UNCHECKED,
tenantId:
String(request.parties?.[0]?.person?.clientId ?? "") ||
String(request.parties?.[1]?.person?.clientId ?? ""),
idempotencyKey: `blame:${requestId}:unchecked:resend:${actorId}`,
});
await this.requestManagementService.applyLinkedClaimsBlameResendStarted(
requestId,
);
@@ -1379,6 +1415,16 @@ export class ExpertBlameService {
);
}
await this.recordBlameExpertActivity({
expertId: String(actorId),
requestId: String(requestId),
eventType: ExpertFileActivityType.HANDLED,
tenantId:
String(request.parties?.[0]?.person?.clientId ?? "") ||
String(request.parties?.[1]?.person?.clientId ?? ""),
idempotencyKey: `blame:${requestId}:handled:${actorId}`,
});
// 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);
@@ -1543,65 +1589,32 @@ export class ExpertBlameService {
}
}
private async updateDamageExpertStats(
expertId: string,
requestId: string,
type: "checked" | "handled",
) {
if (!expertId || !requestId || !["checked", "handled"].includes(type)) {
console.warn("Invalid expertId, requestId, or type");
private async recordBlameExpertActivity(args: {
expertId: string;
requestId: string;
eventType: ExpertFileActivityType;
tenantId?: string;
idempotencyKey?: string;
}) {
const tenantId =
args.tenantId && Types.ObjectId.isValid(args.tenantId)
? args.tenantId
: undefined;
if (
!Types.ObjectId.isValid(args.expertId) ||
!Types.ObjectId.isValid(args.requestId) ||
!tenantId
) {
return;
}
const expert = await this.expertDbService.findOne({
_id: new Types.ObjectId(expertId),
await this.expertFileActivityDbService.recordEvent({
expertId: args.expertId,
tenantId,
fileId: args.requestId,
fileType: ExpertFileKind.BLAME,
eventType: args.eventType,
idempotencyKey: args.idempotencyKey,
});
if (!expert) {
console.warn("Expert not found:", expertId);
return;
}
const requestIdStr = new Types.ObjectId(requestId).toString();
const countedRequestIds =
expert.countedRequests?.map((id) => id.toString()) || [];
if (type === "checked" && countedRequestIds.includes(requestIdStr)) {
console.log(
`Request ${requestIdStr} already checked for expert ${expertId}`,
);
return;
}
const update: any = { $inc: {}, $push: {} };
if (type === "checked") {
update.$inc["requestStats.totalChecked"] = 1;
update.$push["countedRequests"] = requestIdStr;
} else if (type === "handled") {
update.$inc["requestStats.totalHandled"] = 1;
if (countedRequestIds.includes(requestIdStr)) {
update.$inc["requestStats.totalChecked"] = -1;
}
if (!countedRequestIds.includes(requestIdStr)) {
update.$push["countedRequests"] = requestIdStr;
} else {
delete update.$push;
}
}
const updateResult = await this.expertDbService.findOneAndUpdate(
{ _id: new Types.ObjectId(expertId) },
update,
);
if (!updateResult) {
console.warn("Failed to update expert stats for:", expertId);
} else {
console.log(`Expert stats updated (${type}) for expert:`, expertId);
}
}
async replyRequest(requestId: string, reply: SubmitReplyDto, userId: string) {
@@ -1884,11 +1897,15 @@ export class ExpertBlameService {
},
);
await this.expertDbService.updateStats(
actorDetail.sub,
"handled",
requestId,
);
await this.recordBlameExpertActivity({
expertId: String(actorDetail.sub),
requestId: String(requestId),
eventType: ExpertFileActivityType.HANDLED,
tenantId:
String(request.firstPartyDetails?.firstPartyClient?.clientId ?? "") ||
String(request.secondPartyDetails?.secondPartyClient?.clientId ?? ""),
idempotencyKey: `blame:${requestId}:handled:inperson:${actorDetail.sub}`,
});
return updated;
}