1
0
forked from Yara724/api

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

@@ -67,6 +67,11 @@ import {
import { PublicIdService } from "src/utils/public-id/public-id.service";
import { ImageRequiredModel } from "./entites/schema/image-required.schema";
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
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";
import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum";
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";
import { CreationMethod } from "src/request-management/entities/schema/request-management.schema";
@@ -117,6 +122,7 @@ export class ClaimRequestManagementService {
private readonly claimRequestManagementDbService: ClaimRequestManagementDbService,
private readonly claimFactorsImageDbService: ClaimFactorsImageDbService,
private readonly damageExpertDbService: DamageExpertDbService,
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
private readonly branchDbService: BranchDbService,
private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService,
private readonly publicIdService: PublicIdService,
@@ -2577,11 +2583,20 @@ export class ClaimRequestManagementService {
},
);
await this.damageExpertDbService.updateStats(
actorDetail.sub,
"handled",
requestId,
);
if (
Types.ObjectId.isValid(String(actorDetail.sub)) &&
Types.ObjectId.isValid(String(request.userClientKey)) &&
Types.ObjectId.isValid(String(requestId))
) {
await this.expertFileActivityDbService.recordEvent({
expertId: String(actorDetail.sub),
tenantId: String(request.userClientKey),
fileId: String(requestId),
fileType: ExpertFileKind.CLAIM,
eventType: ExpertFileActivityType.HANDLED,
idempotencyKey: `claim:${requestId}:handled:inperson:${actorDetail.sub}`,
});
}
return updated;
}

View File

@@ -2,7 +2,5 @@ import { ExecutionContext, createParamDecorator } from "@nestjs/common";
export const CustomHeader = createParamDecorator(
(data, ctx: ExecutionContext) => {
console.log(ctx.switchToHttp().getRequest());
console.log(ctx.getType());
},
);

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;
}

View File

@@ -67,6 +67,11 @@ import { resendRequestHasPayload } from "src/helpers/claim-expert-resend";
import { snapshotFromDamageExpert } from "src/helpers/expert-profile-snapshot";
import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema";
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
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";
@Injectable()
export class ExpertClaimService {
@@ -191,6 +196,7 @@ export class ExpertClaimService {
private readonly branchDbService: BranchDbService,
private readonly smsOrchestrationService: SmsOrchestrationService,
private readonly userDbService: UserDbService,
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
) { }
/** Load immutable profile fields from `damage-expert` for the acting expert. */
@@ -199,6 +205,30 @@ export class ExpertClaimService {
return snapshotFromDamageExpert(doc as DamageExpertModel);
}
private async recordClaimExpertActivity(args: {
expertId: string;
tenantId: string;
claimId: string;
eventType: ExpertFileActivityType;
idempotencyKey?: string;
}): Promise<void> {
if (
!Types.ObjectId.isValid(args.expertId) ||
!Types.ObjectId.isValid(args.tenantId) ||
!Types.ObjectId.isValid(args.claimId)
) {
return;
}
await this.expertFileActivityDbService.recordEvent({
expertId: args.expertId,
tenantId: args.tenantId,
fileId: args.claimId,
fileType: ExpertFileKind.CLAIM,
eventType: args.eventType,
idempotencyKey: args.idempotencyKey,
});
}
/** 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;
@@ -366,7 +396,6 @@ export class ExpertClaimService {
}
async getClaimRequestsListForExpert(actor): Promise<ClaimListDtoRs> {
console.log(actor);
// const requests = await this.claimRequestManagementDbService.findAllByStatus(
// {
// claimStatus: {
@@ -545,11 +574,13 @@ export class ExpertClaimService {
},
);
await this.damageExpertDbService.updateStats(
actorDetail.sub,
"checked",
requestId,
);
await this.recordClaimExpertActivity({
expertId: String(actorDetail.sub),
tenantId: String(request.userClientKey ?? actorDetail.clientKey ?? ""),
claimId: String(requestId),
eventType: ExpertFileActivityType.CHECKED,
idempotencyKey: `claim:${requestId}:checked:${actorDetail.sub}`,
});
this.unlockApi(request, fifteenMinutes.getTime() - Date.now());
@@ -1420,11 +1451,13 @@ export class ExpertClaimService {
);
if (!isObjection) {
await this.damageExpertDbService.updateStats(
userId.sub,
"handled",
requestId,
);
await this.recordClaimExpertActivity({
expertId: String(userId.sub),
tenantId: String(request.userClientKey ?? userId.clientKey ?? ""),
claimId: String(requestId),
eventType: ExpertFileActivityType.HANDLED,
idempotencyKey: `claim:${requestId}:handled:${userId.sub}`,
});
}
return {
@@ -1492,7 +1525,6 @@ export class ExpertClaimService {
async streamServiceV2(requestId, query, res, header) {
const request = await this.claimCaseDbService.findById(requestId);
console.log(request)
// const blameCase = await this.blameCaseDbService
return request;
// let videoPath: string = "";
@@ -1942,6 +1974,14 @@ export class ExpertClaimService {
$set,
});
await this.recordClaimExpertActivity({
expertId: String(actor.sub),
tenantId: String(actor.clientKey),
claimId: String(claimRequestId),
eventType: ExpertFileActivityType.CHECKED,
idempotencyKey: `claim:${claimRequestId}:checked:${actor.sub}`,
});
const updatedClaim = await this.claimCaseDbService.findById(claimRequestId);
const updatedReply =
replyField === "damageExpertReplyFinal"
@@ -2034,6 +2074,14 @@ export class ExpertClaimService {
},
});
await this.recordClaimExpertActivity({
expertId: String(actor.sub),
tenantId: String(actor.clientKey),
claimId: String(claimRequestId),
eventType: ExpertFileActivityType.UNCHECKED,
idempotencyKey: `claim:${claimRequestId}:unchecked:resend:${actor.sub}`,
});
const ownerPhoneFactors = await this.resolveClaimOwnerPhone(claim);
if (ownerPhoneFactors) {
const expertLastName =
@@ -2076,11 +2124,13 @@ export class ExpertClaimService {
},
);
await this.damageExpertDbService.updateStats(
actorDetail.sub,
"handled",
requestId,
);
await this.recordClaimExpertActivity({
expertId: String(actorDetail.sub),
tenantId: String(request.userClientKey ?? actorDetail.clientKey ?? ""),
claimId: String(requestId),
eventType: ExpertFileActivityType.HANDLED,
idempotencyKey: `claim:${requestId}:handled:${actorDetail.sub}`,
});
return updated;
}
@@ -2459,6 +2509,14 @@ export class ExpertClaimService {
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, updatePayload);
await this.recordClaimExpertActivity({
expertId: String(actor.sub),
tenantId: String(actor.clientKey),
claimId: String(claimRequestId),
eventType: ExpertFileActivityType.HANDLED,
idempotencyKey: `claim:${claimRequestId}:handled:${actor.sub}`,
});
// Same effective step as WAITING_FOR_INSURER_APPROVAL @ INSURER_REVIEW: owner reviews/signs (yara-signature).
if (!needsFactorUpload) {
const ownerPhone = await this.resolveClaimOwnerPhone(claim);
@@ -2556,6 +2614,14 @@ export class ExpertClaimService {
},
});
await this.recordClaimExpertActivity({
expertId: String(actor.sub),
tenantId: String(actor.clientKey),
claimId: String(claimRequestId),
eventType: ExpertFileActivityType.HANDLED,
idempotencyKey: `claim:${claimRequestId}:handled:inperson:${actor.sub}`,
});
return {
claimRequestId,
status: claim.status,
@@ -2992,6 +3058,15 @@ export class ExpertClaimService {
update as any,
{ new: false },
);
if (cleared && lockedById && claim.owner?.userClientKey) {
await this.recordClaimExpertActivity({
expertId: String(lockedById),
tenantId: String(claim.owner.userClientKey),
claimId: String(claimRequestId),
eventType: ExpertFileActivityType.UNCHECKED,
idempotencyKey: `claim:${claimRequestId}:unchecked:expired:${lockedById}`,
});
}
return !!cleared;
}

View File

@@ -98,7 +98,7 @@ export class ExpertInsurerController {
}
@ApiBody({
description: "Detailed expert rating",
description: "Detailed expert rating by insurer",
schema: {
type: "object",
properties: {
@@ -135,7 +135,7 @@ export class ExpertInsurerController {
minimum: 0,
maximum: 5,
example: 4,
description: "برای امتیاز دادن به بات",
description: "برای امتیاز دادن به عملکرد بات",
},
},
required: [

View File

@@ -18,12 +18,15 @@ import { FileRating } from "src/request-management/entities/schema/request-manag
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service";
import { ExpertFileActivityType } from "src/users/entities/schema/expert-file-activity.schema";
@Injectable()
export class ExpertInsurerService {
constructor(
private readonly expertDbService: ExpertDbService,
private readonly damageExpertDbService: DamageExpertDbService,
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
private readonly blameRequestDbService: BlameRequestDbService,
private readonly claimCaseDbService: ClaimCaseDbService,
private readonly branchDbService: BranchDbService,
@@ -37,6 +40,43 @@ export class ExpertInsurerService {
return new Types.ObjectId(raw);
}
private async buildExpertActivityStatsMap(
tenantId: Types.ObjectId,
expertIds: string[],
): Promise<Record<string, { totalHandled: number; totalChecked: number }>> {
const events = await this.expertFileActivityDbService.findByTenantAndExperts(
tenantId,
expertIds,
);
const stateByExpertFile = new Map<string, { checked: boolean; handled: boolean }>();
for (const e of events) {
const key = `${String(e.expertId)}:${String(e.fileId)}`;
const prev = stateByExpertFile.get(key) ?? { checked: false, handled: false };
if (e.eventType === ExpertFileActivityType.CHECKED) {
if (!prev.handled) prev.checked = true;
} else if (e.eventType === ExpertFileActivityType.UNCHECKED) {
prev.checked = false;
} else if (e.eventType === ExpertFileActivityType.HANDLED) {
prev.handled = true;
prev.checked = false;
}
stateByExpertFile.set(key, prev);
}
const result: Record<string, { totalHandled: number; totalChecked: number }> = {};
for (const expertId of expertIds) {
result[expertId] = { totalHandled: 0, totalChecked: 0 };
}
for (const [key, st] of stateByExpertFile.entries()) {
const [expertId] = key.split(":");
if (!result[expertId]) result[expertId] = { totalHandled: 0, totalChecked: 0 };
if (st.handled) result[expertId].totalHandled += 1;
else if (st.checked) result[expertId].totalChecked += 1;
}
return result;
}
private parseObjectId(value: string, label: string): Types.ObjectId {
if (!Types.ObjectId.isValid(value)) {
throw new BadRequestException(`Invalid ${label}`);
@@ -141,6 +181,11 @@ export class ExpertInsurerService {
]);
const allExpertsRaw = [...experts, ...damageExperts];
const expertIds = allExpertsRaw.map((e) => String(e._id));
const expertActivityStatsMap = await this.buildExpertActivityStatsMap(
clientObjectId,
expertIds,
);
const expertTotalRatingsMap: Record<string, number[]> = {};
const expertRatingsByCategoryMap: Record<string, Record<string, number[]>> = {};
@@ -193,7 +238,10 @@ export class ExpertInsurerService {
fullName: `${expert.firstName} ${expert.lastName}`,
role: expert.role,
type: expert.userType,
requestStats: expert.requestStats,
requestStats: expertActivityStatsMap[expertIdStr] ?? {
totalHandled: 0,
totalChecked: 0,
},
createdAt: expert.createdAt,
overallAverageRating,
averageRatingsByCategory,

View File

@@ -47,10 +47,6 @@ export class ProfileController {
@UseGuards(GlobalGuard)
@ApiBody({ type: AddPlateProfileDto })
async addPlate(@CurrentUser() user, @Body() body: AddPlateDto) {
console.log(
"🚀 ~ file: profile.controller.ts:50 ~ ProfileController ~ addPlate ~ user:",
user,
);
return await this.plateService.addPlate(user.sub, body);
}

View File

@@ -71,6 +71,11 @@ import { WorkflowStepDbService } from "src/workflow-step-management/entities/db-
import { WorkflowStepModel } from "src/workflow-step-management/entities/schema/workflow-step.schema";
import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatus.enum";
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.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";
import { UploadContext } from "./entities/schema/blame-voice.schema";
import { HashService } from "src/utils/hash/hash.service";
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
@@ -309,6 +314,7 @@ export class RequestManagementService {
private readonly userSign: UserSignDbService,
private readonly smsOrchestrationService: SmsOrchestrationService,
private readonly expertDbService: ExpertDbService,
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
private readonly autoCloseRequestService: AutoCloseRequestService,
private readonly claimRequestManagementDbService: ClaimRequestManagementDbService,
private readonly claimCaseDbService: ClaimCaseDbService,
@@ -2370,9 +2376,6 @@ export class RequestManagementService {
}
if (p.firstPartyDetails.firstPartyPhoneNumber == phoneNumber) {
console.log(
new Date(p.createdAt).toLocaleTimeString("fa-IR").split(","),
);
res.listOfFirstParty.push({
id: p.id,
blameStatus: p.blameStatus,
@@ -2746,76 +2749,28 @@ export class RequestManagementService {
return null;
}
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;
tenantId: string;
requestId: string;
eventType: ExpertFileActivityType;
idempotencyKey?: string;
}): Promise<void> {
if (
!Types.ObjectId.isValid(args.expertId) ||
!Types.ObjectId.isValid(args.tenantId) ||
!Types.ObjectId.isValid(args.requestId)
) {
return;
}
// Validate that both expertId and requestId are valid ObjectId strings
if (!Types.ObjectId.isValid(expertId)) {
console.warn("Invalid ObjectId format for expertId:", expertId);
return;
}
if (!Types.ObjectId.isValid(requestId)) {
console.warn("Invalid ObjectId format for requestId:", requestId);
return;
}
const expert = await this.expertDbService.findOne({
_id: new Types.ObjectId(expertId),
await this.expertFileActivityDbService.recordEvent({
expertId: args.expertId,
tenantId: args.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);
}
}
private async handleDisagreement(
@@ -3006,11 +2961,17 @@ export class RequestManagementService {
request._id.toString() :
String(request._id);
await this.updateDamageExpertStats(
actorIdStr,
requestIdStr,
"handled",
);
const tenantIdStr =
request.firstPartyDetails?.firstPartyClient?.clientId?.toString?.() ||
request.secondPartyDetails?.secondPartyClient?.clientId?.toString?.() ||
"";
await this.recordBlameExpertActivity({
expertId: actorIdStr,
tenantId: tenantIdStr,
requestId: requestIdStr,
eventType: ExpertFileActivityType.HANDLED,
idempotencyKey: `blame:${requestIdStr}:handled:${actorIdStr}`,
});
}
await this.requestManagementDbService.findByIdAndUpdate(request._id, {
$set: { isHandledStatsUpdated: true },

View File

@@ -47,7 +47,7 @@ export class SmsOrchestrationService implements OnModuleInit {
}
buildClaimLink(claimRequestId: string): string {
return `${process.env.URL}/claim?token=${claimRequestId}`;
return `${process.env.URL}/caseClaim?token=${claimRequestId}`;
}
async sendInviteLink(phoneNumber: string, link: string): Promise<boolean> {

View File

@@ -46,64 +46,6 @@ export class DamageExpertDbService {
.exec();
}
async updateStats(
expertId: string,
type: "checked" | "handled",
requestId: string,
) {
if (!expertId || !requestId || !["checked", "handled"].includes(type)) {
console.warn("Invalid expertId, requestId, or type");
return;
}
const expert = await this.damageExpertModel.findById(expertId);
if (!expert) {
console.warn("Expert not found:", expertId);
return;
}
const requestIdStr = new Types.ObjectId(requestId).toString();
const countedRequests =
expert.countedRequests?.map((r) => r.toString()) || [];
if (type === "checked" && countedRequests.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 (countedRequests.includes(requestIdStr)) {
update.$inc["requestStats.totalChecked"] = -1;
}
if (!countedRequests.includes(requestIdStr)) {
update.$push["countedRequests"] = requestIdStr;
} else {
delete update.$push;
}
}
const result = await this.damageExpertModel.findByIdAndUpdate(
expertId,
update,
);
if (!result) {
console.warn("Failed to update stats for expert:", expertId);
} else {
console.log(`Stats updated (${type}) for expert:`, expertId);
}
}
async updateOne(filter: any, update: any) {
return this.damageExpertModel.updateOne(filter, update);
}

View File

@@ -0,0 +1,73 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model, Types } from "mongoose";
import {
ExpertFileActivity,
ExpertFileActivityDocument,
ExpertFileActivityType,
ExpertFileKind,
} from "../schema/expert-file-activity.schema";
@Injectable()
export class ExpertFileActivityDbService {
constructor(
@InjectModel(ExpertFileActivity.name)
private readonly activityModel: Model<ExpertFileActivityDocument>,
) {}
async recordEvent(args: {
expertId: string | Types.ObjectId;
tenantId: string | Types.ObjectId;
fileId: string | Types.ObjectId;
fileType: ExpertFileKind;
eventType: ExpertFileActivityType;
occurredAt?: Date;
idempotencyKey?: string;
}): Promise<void> {
const payload = {
expertId: new Types.ObjectId(String(args.expertId)),
tenantId: new Types.ObjectId(String(args.tenantId)),
fileId: new Types.ObjectId(String(args.fileId)),
fileType: args.fileType,
eventType: args.eventType,
occurredAt: args.occurredAt ?? new Date(),
...(args.idempotencyKey ? { idempotencyKey: args.idempotencyKey } : {}),
};
if (args.idempotencyKey) {
await this.activityModel.updateOne(
{ idempotencyKey: args.idempotencyKey },
{ $setOnInsert: payload },
{ upsert: true },
);
return;
}
await this.activityModel.create(payload);
}
async findByTenantAndExperts(
tenantId: string | Types.ObjectId,
expertIds: Array<string | Types.ObjectId>,
): Promise<
Array<{
expertId: Types.ObjectId;
fileId: Types.ObjectId;
eventType: ExpertFileActivityType;
occurredAt: Date;
}>
> {
if (expertIds.length === 0) return [];
const ids = expertIds.map((id) => new Types.ObjectId(String(id)));
return this.activityModel
.find(
{
tenantId: new Types.ObjectId(String(tenantId)),
expertId: { $in: ids },
},
{ expertId: 1, fileId: 1, eventType: 1, occurredAt: 1 },
)
.sort({ occurredAt: 1, _id: 1 })
.lean();
}
}

View File

@@ -33,50 +33,6 @@ export class ExpertDbService {
return await this.expertModel.find(filter).lean();
}
async updateStats(
expertId: string,
type: "checked" | "handled",
requestId: string,
) {
if (!expertId || !requestId || !["checked", "handled"].includes(type)) {
console.warn("Invalid expertId, requestId, or type");
return;
}
const expert = await this.expertModel.findById(expertId);
if (!expert) {
console.warn("Expert not found:", expertId);
return;
}
const requestIdStr = new Types.ObjectId(requestId).toString();
const counted = (expert.countedRequests || []).map((r) => r.toString());
if (counted.includes(requestIdStr)) {
console.log("Request already counted for expert:", requestIdStr);
return;
}
const updateField =
type === "handled"
? {
"requestStats.totalHandled": 1,
"requestStats.totalChecked": -1,
}
: { "requestStats.totalChecked": 1 };
const result = await this.expertModel.findByIdAndUpdate(expertId, {
$inc: updateField,
$push: { countedRequests: requestIdStr },
});
if (!result) {
console.warn("Failed to update expert stats for:", expertId);
} else {
console.log("Updated stats for expert:", expertId);
}
}
async updateOne(filter: any, update: any) {
return this.expertModel.updateOne(filter, update);
}

View File

@@ -8,16 +8,6 @@ import {
import { RoleEnum } from "src/Types&Enums/role.enum";
import { UserType } from "src/Types&Enums/userType.enum";
@Schema({ _id: false })
export class RequestStats {
@Prop({ default: 0 })
totalHandled: number;
@Prop({ default: 0 })
totalChecked: number;
}
const RequestStatsSchema = SchemaFactory.createForClass(RequestStats);
@Schema({ _id: false })
export class PersonalInfo {
@@ -166,19 +156,6 @@ export class DamageExpertModel {
@Prop({ type: "string" })
city: string;
@Prop({ type: RequestStatsSchema, default: () => ({}) })
requestStats: RequestStats;
@Prop({
type: [{ type: String }],
default: [],
validate: {
validator: (arr: any[]) => arr.every((val) => typeof val === "string"),
message: "All countedRequests must be strings",
},
})
countedRequests?: string[];
createdAt: Date;
}
@@ -190,11 +167,4 @@ DamageExpertDbSchema.pre("save", function (next) {
this.username = this.email;
}
next();
});
DamageExpertDbSchema.pre("save", function (next) {
if (!this.requestStats) {
this.requestStats = { totalChecked: 0, totalHandled: 0 };
}
next();
});
});

View File

@@ -0,0 +1,52 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument, Types } from "mongoose";
export type ExpertFileActivityDocument = HydratedDocument<ExpertFileActivity>;
export enum ExpertFileActivityType {
CHECKED = "checked",
HANDLED = "handled",
UNCHECKED = "unchecked",
}
export enum ExpertFileKind {
BLAME = "blame",
CLAIM = "claim",
}
@Schema({ collection: "expertFileActivities", timestamps: true })
export class ExpertFileActivity {
@Prop({ type: Types.ObjectId, required: true, index: true })
expertId: Types.ObjectId;
@Prop({ type: Types.ObjectId, required: true, index: true })
tenantId: Types.ObjectId;
@Prop({ type: Types.ObjectId, required: true, index: true })
fileId: Types.ObjectId;
@Prop({ type: String, enum: ExpertFileKind, required: true, index: true })
fileType: ExpertFileKind;
@Prop({
type: String,
enum: ExpertFileActivityType,
required: true,
index: true,
})
eventType: ExpertFileActivityType;
@Prop({ type: Date, default: Date.now, index: true })
occurredAt: Date;
@Prop({ type: String, required: false, index: true })
idempotencyKey?: string;
}
export const ExpertFileActivitySchema =
SchemaFactory.createForClass(ExpertFileActivity);
ExpertFileActivitySchema.index(
{ idempotencyKey: 1 },
{ unique: true, partialFilterExpression: { idempotencyKey: { $exists: true } } },
);

View File

@@ -3,17 +3,6 @@ import { Degrees } from "src/Types&Enums/degrees.enum";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { UserType } from "src/Types&Enums/userType.enum";
@Schema({ _id: false })
export class RequestStats {
@Prop({ default: 0 })
totalHandled: number;
@Prop({ default: 0 })
totalChecked: number;
}
const RequestStatsSchema = SchemaFactory.createForClass(RequestStats);
@Schema({ collection: "expert", versionKey: false, timestamps: true })
export class ExpertModel {
@Prop({})
@@ -79,27 +68,8 @@ export class ExpertModel {
@Prop({ type: "string" })
otp: string;
@Prop({ type: RequestStatsSchema, default: () => ({}) })
requestStats: RequestStats;
@Prop({
type: [{ type: String }],
default: [],
validate: {
validator: (arr: any[]) => arr.every((val) => typeof val === "string"),
message: "All countedRequests must be strings",
},
})
countedRequests?: string[];
createdAt: Date;
}
export const ExpertDbSchema = SchemaFactory.createForClass(ExpertModel);
ExpertDbSchema.pre("save", function (next) {
if (!this.requestStats) {
this.requestStats = { totalChecked: 0, totalHandled: 0 };
}
next();
});

View File

@@ -2,17 +2,6 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Types } from "mongoose";
import { RoleEnum } from "src/Types&Enums/role.enum";
@Schema({ _id: false })
export class RequestStats {
@Prop({ default: 0 })
totalHandled: number;
@Prop({ default: 0 })
totalChecked: number;
}
const RequestStatsSchema = SchemaFactory.createForClass(RequestStats);
@Schema({
collection: "field-expert",
versionKey: false,
@@ -48,31 +37,8 @@ export class FieldExpertModel {
@Prop({ type: "string", default: "" })
otp: string;
@Prop({ type: RequestStatsSchema, default: () => ({}) })
requestStats: RequestStats;
@Prop({
type: [{ type: String }],
default: [],
validate: {
validator: (arr: any[]) => arr.every((val) => typeof val === "string"),
message: "All countedRequests must be strings",
},
})
countedRequests?: string[];
createdAt: Date;
}
export const FieldExpertDbSchema =
SchemaFactory.createForClass(FieldExpertModel);
FieldExpertDbSchema.pre("save", function (next) {
if (!this.username) {
this.username = this.email;
}
if (!this.requestStats) {
this.requestStats = { totalChecked: 0, totalHandled: 0 };
}
next();
});
SchemaFactory.createForClass(FieldExpertModel);

View File

@@ -9,10 +9,15 @@ import { FieldExpertDbService } from "./entities/db-service/field-expert.db.serv
import { InsurerExpertDbService } from "./entities/db-service/insurer-expert.db.service";
import { RegistrarDbService } from "./entities/db-service/registrar.db.service";
import { UserDbService } from "./entities/db-service/user.db.service";
import { ExpertFileActivityDbService } from "./entities/db-service/expert-file-activity.db.service";
import {
DamageExpertDbSchema,
DamageExpertModel,
} from "./entities/schema/damage-expert.schema";
import {
ExpertFileActivity,
ExpertFileActivitySchema,
} from "./entities/schema/expert-file-activity.schema";
import { ExpertDbSchema } from "./entities/schema/expert.schema";
import {
FieldExpertDbSchema,
@@ -37,6 +42,7 @@ import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
{ name: FieldExpertModel.name, schema: FieldExpertDbSchema },
{ name: InsurerExpertModel.name, schema: InsurerExpertDbSchema },
{ name: RegistrarModel.name, schema: RegistrarDbSchema },
{ name: ExpertFileActivity.name, schema: ExpertFileActivitySchema },
]),
OtpModule,
HashModule,
@@ -49,6 +55,7 @@ import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
FieldExpertDbService,
InsurerExpertDbService,
RegistrarDbService,
ExpertFileActivityDbService,
],
exports: [
UserDbService,
@@ -57,6 +64,7 @@ import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
FieldExpertDbService,
InsurerExpertDbService,
RegistrarDbService,
ExpertFileActivityDbService,
],
})
export class UsersModule {}