diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 02852c1..a93a0ec 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -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; } diff --git a/src/decorators/customeHeader.decorator.ts b/src/decorators/customeHeader.decorator.ts index 83e1f8f..31d9cb7 100644 --- a/src/decorators/customeHeader.decorator.ts +++ b/src/decorators/customeHeader.decorator.ts @@ -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()); }, ); diff --git a/src/expert-blame/expert-blame.service.ts b/src/expert-blame/expert-blame.service.ts index a80f034..2afe394 100644 --- a/src/expert-blame/expert-blame.service.ts +++ b/src/expert-blame/expert-blame.service.ts @@ -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; } diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 79e3f49..07efb79 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -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 { + 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 { if (!claim?.owner?.userId) return undefined; @@ -366,7 +396,6 @@ export class ExpertClaimService { } async getClaimRequestsListForExpert(actor): Promise { - 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; } diff --git a/src/expert-insurer/expert-insurer.controller.ts b/src/expert-insurer/expert-insurer.controller.ts index 5808ac5..3fa567c 100644 --- a/src/expert-insurer/expert-insurer.controller.ts +++ b/src/expert-insurer/expert-insurer.controller.ts @@ -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: [ diff --git a/src/expert-insurer/expert-insurer.service.ts b/src/expert-insurer/expert-insurer.service.ts index cda0370..49860f9 100644 --- a/src/expert-insurer/expert-insurer.service.ts +++ b/src/expert-insurer/expert-insurer.service.ts @@ -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> { + const events = await this.expertFileActivityDbService.findByTenantAndExperts( + tenantId, + expertIds, + ); + const stateByExpertFile = new Map(); + + 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 = {}; + 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 = {}; const expertRatingsByCategoryMap: Record> = {}; @@ -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, diff --git a/src/profile/profile.controller.ts b/src/profile/profile.controller.ts index 10c4812..8b60c7a 100644 --- a/src/profile/profile.controller.ts +++ b/src/profile/profile.controller.ts @@ -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); } diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 5f6c635..608bd91 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -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 { + 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 }, diff --git a/src/sms-orchestration/sms-orchestration.service.ts b/src/sms-orchestration/sms-orchestration.service.ts index d2258f7..20f3925 100644 --- a/src/sms-orchestration/sms-orchestration.service.ts +++ b/src/sms-orchestration/sms-orchestration.service.ts @@ -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 { diff --git a/src/users/entities/db-service/damage-expert.db.service.ts b/src/users/entities/db-service/damage-expert.db.service.ts index 936e9e8..13ce8b1 100644 --- a/src/users/entities/db-service/damage-expert.db.service.ts +++ b/src/users/entities/db-service/damage-expert.db.service.ts @@ -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); } diff --git a/src/users/entities/db-service/expert-file-activity.db.service.ts b/src/users/entities/db-service/expert-file-activity.db.service.ts new file mode 100644 index 0000000..f62c031 --- /dev/null +++ b/src/users/entities/db-service/expert-file-activity.db.service.ts @@ -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, + ) {} + + async recordEvent(args: { + expertId: string | Types.ObjectId; + tenantId: string | Types.ObjectId; + fileId: string | Types.ObjectId; + fileType: ExpertFileKind; + eventType: ExpertFileActivityType; + occurredAt?: Date; + idempotencyKey?: string; + }): Promise { + 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, + ): 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(); + } +} diff --git a/src/users/entities/db-service/expert.db.service.ts b/src/users/entities/db-service/expert.db.service.ts index 5b86a00..bdc25a0 100644 --- a/src/users/entities/db-service/expert.db.service.ts +++ b/src/users/entities/db-service/expert.db.service.ts @@ -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); } diff --git a/src/users/entities/schema/damage-expert.schema.ts b/src/users/entities/schema/damage-expert.schema.ts index 380f309..58fa125 100644 --- a/src/users/entities/schema/damage-expert.schema.ts +++ b/src/users/entities/schema/damage-expert.schema.ts @@ -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(); -}); +}); \ No newline at end of file diff --git a/src/users/entities/schema/expert-file-activity.schema.ts b/src/users/entities/schema/expert-file-activity.schema.ts new file mode 100644 index 0000000..2554dec --- /dev/null +++ b/src/users/entities/schema/expert-file-activity.schema.ts @@ -0,0 +1,52 @@ +import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; +import { HydratedDocument, Types } from "mongoose"; + +export type ExpertFileActivityDocument = HydratedDocument; + +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 } } }, +); diff --git a/src/users/entities/schema/expert.schema.ts b/src/users/entities/schema/expert.schema.ts index 31f6589..71163ec 100644 --- a/src/users/entities/schema/expert.schema.ts +++ b/src/users/entities/schema/expert.schema.ts @@ -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(); -}); diff --git a/src/users/entities/schema/field-expert.schema.ts b/src/users/entities/schema/field-expert.schema.ts index dd5838d..ff7dc63 100644 --- a/src/users/entities/schema/field-expert.schema.ts +++ b/src/users/entities/schema/field-expert.schema.ts @@ -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); \ No newline at end of file diff --git a/src/users/users.module.ts b/src/users/users.module.ts index aa050b7..db86804 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -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 {}