forked from Yara724/api
Compare commits
7 Commits
b826a133a4
...
c6f7edabd2
| Author | SHA1 | Date | |
|---|---|---|---|
| c6f7edabd2 | |||
| 75b7e5ecad | |||
| a3156881b8 | |||
| 41c44dcf23 | |||
| b63c2155bc | |||
| 9463142ecf | |||
| a71c4ea980 |
@@ -1,5 +1,9 @@
|
|||||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
import { Schema as MongooseSchema, Types } from "mongoose";
|
import { Schema as MongooseSchema, Types } from "mongoose";
|
||||||
|
import {
|
||||||
|
ExpertProfileSnapshot,
|
||||||
|
ExpertProfileSnapshotSchema,
|
||||||
|
} from "src/request-management/entities/schema/expert-profile-snapshot.schema";
|
||||||
import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum";
|
import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum";
|
||||||
import { UserReplyEnum } from "src/Types&Enums/claim-request-management/userReply.enum";
|
import { UserReplyEnum } from "src/Types&Enums/claim-request-management/userReply.enum";
|
||||||
|
|
||||||
@@ -8,8 +12,12 @@ export class ClaimPartPricing {
|
|||||||
@Prop({ type: String })
|
@Prop({ type: String })
|
||||||
partId: string;
|
partId: string;
|
||||||
|
|
||||||
@Prop({ type: String })
|
/**
|
||||||
carPartDamage?: string;
|
* Legacy string or structured `{ part, side }` from expert reply DTOs (V1/V2).
|
||||||
|
* Must be Mixed — objects cannot cast to String.
|
||||||
|
*/
|
||||||
|
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||||
|
carPartDamage?: string | { part?: string; side?: string };
|
||||||
|
|
||||||
@Prop({ type: String })
|
@Prop({ type: String })
|
||||||
typeOfDamage?: string;
|
typeOfDamage?: string;
|
||||||
@@ -23,8 +31,9 @@ export class ClaimPartPricing {
|
|||||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||||
totalPayment?: string | number;
|
totalPayment?: string | number;
|
||||||
|
|
||||||
|
/** V1/V2: string/number legacy, or `{ option, price?, branchId? }` after expert normalization. */
|
||||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||||
daghi?: string | number;
|
daghi?: string | number | Record<string, unknown>;
|
||||||
|
|
||||||
@Prop({ type: Boolean, default: false })
|
@Prop({ type: Boolean, default: false })
|
||||||
factorNeeded?: boolean;
|
factorNeeded?: boolean;
|
||||||
@@ -83,6 +92,10 @@ export class ClaimExpertReply {
|
|||||||
|
|
||||||
@Prop({ type: ClaimUserCommentSchema })
|
@Prop({ type: ClaimUserCommentSchema })
|
||||||
userComment?: ClaimUserComment;
|
userComment?: ClaimUserComment;
|
||||||
|
|
||||||
|
/** Damage expert profile at reply time (from `damage-expert` collection). */
|
||||||
|
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||||
|
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||||
}
|
}
|
||||||
export const ClaimExpertReplySchema =
|
export const ClaimExpertReplySchema =
|
||||||
SchemaFactory.createForClass(ClaimExpertReply);
|
SchemaFactory.createForClass(ClaimExpertReply);
|
||||||
@@ -160,6 +173,10 @@ export class ClaimResendRequest {
|
|||||||
/** Set when the owner has satisfied every requested document/part (or acknowledged description-only resend). */
|
/** Set when the owner has satisfied every requested document/part (or acknowledged description-only resend). */
|
||||||
@Prop({ type: Date })
|
@Prop({ type: Date })
|
||||||
fulfilledAt?: Date;
|
fulfilledAt?: Date;
|
||||||
|
|
||||||
|
/** Damage expert profile when resend was requested (`damage-expert` collection). */
|
||||||
|
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||||
|
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||||
}
|
}
|
||||||
export const ClaimResendRequestSchema =
|
export const ClaimResendRequestSchema =
|
||||||
SchemaFactory.createForClass(ClaimResendRequest);
|
SchemaFactory.createForClass(ClaimResendRequest);
|
||||||
@@ -239,6 +256,14 @@ export class ClaimEvaluation {
|
|||||||
|
|
||||||
@Prop({ type: ClaimPriceDropSchema })
|
@Prop({ type: ClaimPriceDropSchema })
|
||||||
priceDrop?: ClaimPriceDrop;
|
priceDrop?: ClaimPriceDrop;
|
||||||
|
|
||||||
|
/** Damage expert profile at last factor validation (`damage-expert` collection). */
|
||||||
|
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||||
|
factorValidationExpertProfileSnapshot?: ExpertProfileSnapshot;
|
||||||
|
|
||||||
|
/** Damage expert profile when in-person visit was requested (`damage-expert` collection). */
|
||||||
|
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||||
|
inPersonVisitExpertProfileSnapshot?: ExpertProfileSnapshot;
|
||||||
}
|
}
|
||||||
export const ClaimEvaluationSchema =
|
export const ClaimEvaluationSchema =
|
||||||
SchemaFactory.createForClass(ClaimEvaluation);
|
SchemaFactory.createForClass(ClaimEvaluation);
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||||
|
import {
|
||||||
|
ExpertProfileSnapshot,
|
||||||
|
ExpertProfileSnapshotSchema,
|
||||||
|
} from "src/request-management/entities/schema/expert-profile-snapshot.schema";
|
||||||
|
|
||||||
@Schema({ _id: false })
|
@Schema({ _id: false })
|
||||||
export class ClaimActorLock {
|
export class ClaimActorLock {
|
||||||
@@ -12,6 +16,10 @@ export class ClaimActorLock {
|
|||||||
|
|
||||||
@Prop({ type: String, default: "damage_expert" })
|
@Prop({ type: String, default: "damage_expert" })
|
||||||
actorRole: "damage_expert" | "expert" | "admin";
|
actorRole: "damage_expert" | "expert" | "admin";
|
||||||
|
|
||||||
|
/** Damage expert profile when lock was taken (`damage-expert` collection). */
|
||||||
|
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||||
|
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||||
}
|
}
|
||||||
export const ClaimActorLockSchema = SchemaFactory.createForClass(ClaimActorLock);
|
export const ClaimActorLockSchema = SchemaFactory.createForClass(ClaimActorLock);
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ export class SubmitReply {
|
|||||||
|
|
||||||
@Prop()
|
@Prop()
|
||||||
userComment?: UserComment;
|
userComment?: UserComment;
|
||||||
|
|
||||||
|
/** Damage expert profile at reply time (`damage-expert` collection). */
|
||||||
|
@Prop({ type: Object })
|
||||||
|
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ActorLockDetails {
|
export class ActorLockDetails {
|
||||||
@@ -130,6 +134,10 @@ export class ActorLockDetails {
|
|||||||
|
|
||||||
@Prop({ type: Types.ObjectId })
|
@Prop({ type: Types.ObjectId })
|
||||||
actorId?: Types.ObjectId;
|
actorId?: Types.ObjectId;
|
||||||
|
|
||||||
|
/** Damage expert profile when lock was taken (`damage-expert` collection). */
|
||||||
|
@Prop({ type: Object })
|
||||||
|
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResendCarPartsDto {
|
export class ResendCarPartsDto {
|
||||||
@@ -149,6 +157,10 @@ export class ClaimSubmitResend {
|
|||||||
|
|
||||||
@Prop({ required: true })
|
@Prop({ required: true })
|
||||||
resendCarParts: ResendCarPartsDto[];
|
resendCarParts: ResendCarPartsDto[];
|
||||||
|
|
||||||
|
/** Damage expert profile when resend was requested (`damage-expert` collection). */
|
||||||
|
@Prop({ type: Object })
|
||||||
|
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserResendDocuments {
|
export class UserResendDocuments {
|
||||||
@@ -360,6 +372,14 @@ export class ClaimRequestManagementModel {
|
|||||||
@Prop({ type: UserClaimRating, required: false })
|
@Prop({ type: UserClaimRating, required: false })
|
||||||
userRating?: UserClaimRating;
|
userRating?: UserClaimRating;
|
||||||
|
|
||||||
|
/** Latest damage expert profile when validating repair factors (V1). */
|
||||||
|
@Prop({ type: Object })
|
||||||
|
factorValidationExpertProfileSnapshot?: Record<string, string | undefined>;
|
||||||
|
|
||||||
|
/** Damage expert profile when in-person visit was requested (V1). */
|
||||||
|
@Prop({ type: Object })
|
||||||
|
damageExpertInPersonVisitProfileSnapshot?: Record<string, string | undefined>;
|
||||||
|
|
||||||
@Prop({ type: String, required: false })
|
@Prop({ type: String, required: false })
|
||||||
visitLocation?: string;
|
visitLocation?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import {
|
|||||||
blameCaseAccessibleToExpert,
|
blameCaseAccessibleToExpert,
|
||||||
requireActorClientKey,
|
requireActorClientKey,
|
||||||
} from "src/helpers/tenant-scope";
|
} from "src/helpers/tenant-scope";
|
||||||
|
import {
|
||||||
|
blameCaseStatusToReportBucket,
|
||||||
|
initialBlameExpertReportBuckets,
|
||||||
|
} from "src/helpers/expert-panel-status-report";
|
||||||
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
||||||
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
|
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +43,8 @@ import { ExpertDbService } from "src/users/entities/db-service/expert.db.service
|
|||||||
import { BlameDocumentDbService } from "src/request-management/entities/db-service/blame-document.db.service";
|
import { BlameDocumentDbService } from "src/request-management/entities/db-service/blame-document.db.service";
|
||||||
import { UserSignDbService } from "src/request-management/entities/db-service/sign.db.service";
|
import { UserSignDbService } from "src/request-management/entities/db-service/sign.db.service";
|
||||||
import { RequestManagementService } from "src/request-management/request-management.service";
|
import { RequestManagementService } from "src/request-management/request-management.service";
|
||||||
|
import { snapshotFromFieldExpert } from "src/helpers/expert-profile-snapshot";
|
||||||
|
import { ExpertModel } from "src/users/entities/schema/expert.schema";
|
||||||
|
|
||||||
interface CheckedRequestEntry {
|
interface CheckedRequestEntry {
|
||||||
CheckedRequest?: {
|
CheckedRequest?: {
|
||||||
@@ -81,6 +87,14 @@ export class ExpertBlameService {
|
|||||||
private readonly requestManagementService: RequestManagementService,
|
private readonly requestManagementService: RequestManagementService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
/** Load immutable profile fields from `expert` for the acting field expert. */
|
||||||
|
private async snapshotFieldExpert(actorId: string) {
|
||||||
|
const expertDoc = await this.expertDbService.findOne({
|
||||||
|
_id: new Types.ObjectId(actorId),
|
||||||
|
});
|
||||||
|
return snapshotFromFieldExpert(expertDoc as ExpertModel);
|
||||||
|
}
|
||||||
|
|
||||||
async findAll(actor: any): Promise<AllRequestDtoRs> {
|
async findAll(actor: any): Promise<AllRequestDtoRs> {
|
||||||
// 1. Fetch all potentially relevant requests from the database.
|
// 1. Fetch all potentially relevant requests from the database.
|
||||||
// Exclude CAR_BODY type requests as they are automatically handled and don't need expert review
|
// Exclude CAR_BODY type requests as they are automatically handled and don't need expert review
|
||||||
@@ -195,6 +209,38 @@ export class ExpertBlameService {
|
|||||||
* 2. Requests where current expert made the decision
|
* 2. Requests where current expert made the decision
|
||||||
* Does NOT show requests decided by other experts.
|
* Does NOT show requests decided by other experts.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* V2: Count blame cases for this expert’s tenant, grouped for dashboard:
|
||||||
|
* `IN_PROGRESS` = OPEN + WAITING_FOR_SECOND_PARTY; other {@link CaseStatus} keys unchanged.
|
||||||
|
*/
|
||||||
|
async getStatusReportBucketsV2(actor: any): Promise<Record<string, number>> {
|
||||||
|
requireActorClientKey(actor);
|
||||||
|
const rows = await this.blameRequestDbService.find({}, { lean: true });
|
||||||
|
const buckets = initialBlameExpertReportBuckets();
|
||||||
|
for (const doc of rows as Record<string, unknown>[]) {
|
||||||
|
if (!this.blameDocIncludedInExpertTenantReport(doc, actor)) continue;
|
||||||
|
const st = String(doc.status ?? "");
|
||||||
|
const key = blameCaseStatusToReportBucket(st);
|
||||||
|
buckets.all++;
|
||||||
|
buckets[key] = (buckets[key] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
return buckets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tenant + expert-initiated visibility (same as list, without queue filters). */
|
||||||
|
private blameDocIncludedInExpertTenantReport(
|
||||||
|
doc: Record<string, unknown>,
|
||||||
|
actor: { sub: string; clientKey?: string },
|
||||||
|
): boolean {
|
||||||
|
if (!blameCaseAccessibleToExpert(doc, actor)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (doc.expertInitiated && doc.initiatedByFieldExpertId) {
|
||||||
|
return String(doc.initiatedByFieldExpertId) === String(actor.sub);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async findAllV2(actor: any): Promise<AllRequestDtoRsV2> {
|
async findAllV2(actor: any): Promise<AllRequestDtoRsV2> {
|
||||||
try {
|
try {
|
||||||
requireActorClientKey(actor);
|
requireActorClientKey(actor);
|
||||||
@@ -767,6 +813,7 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
||||||
|
const lockSnapshot = await this.snapshotFieldExpert(actorDetail.sub);
|
||||||
|
|
||||||
const updateResult = await this.requestManagementDbService.findOneAndUpdate(
|
const updateResult = await this.requestManagementDbService.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
@@ -782,6 +829,7 @@ export class ExpertBlameService {
|
|||||||
actorLocked: {
|
actorLocked: {
|
||||||
fullName: actorDetail.fullName,
|
fullName: actorDetail.fullName,
|
||||||
actorId: new Types.ObjectId(actorDetail.sub),
|
actorId: new Types.ObjectId(actorDetail.sub),
|
||||||
|
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
$push: {
|
$push: {
|
||||||
@@ -872,6 +920,8 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lockSnapshot = await this.snapshotFieldExpert(actorDetail.sub);
|
||||||
|
|
||||||
// Lock the request (either unlocked or expired lock)
|
// Lock the request (either unlocked or expired lock)
|
||||||
const updateResult = await this.blameRequestDbService.findByIdAndUpdate(
|
const updateResult = await this.blameRequestDbService.findByIdAndUpdate(
|
||||||
requestId,
|
requestId,
|
||||||
@@ -883,6 +933,7 @@ export class ExpertBlameService {
|
|||||||
actorId: new Types.ObjectId(actorDetail.sub),
|
actorId: new Types.ObjectId(actorDetail.sub),
|
||||||
actorName: actorDetail.fullName || "Unknown Expert",
|
actorName: actorDetail.fullName || "Unknown Expert",
|
||||||
actorRole: "expert",
|
actorRole: "expert",
|
||||||
|
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1004,6 +1055,7 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const resendSnapshot = await this.snapshotFieldExpert(actorId);
|
||||||
|
|
||||||
const partyResendRequests = resendDto.parties.map((party) => {
|
const partyResendRequests = resendDto.parties.map((party) => {
|
||||||
const uid = String(party.partyId);
|
const uid = String(party.partyId);
|
||||||
@@ -1026,6 +1078,7 @@ export class ExpertBlameService {
|
|||||||
parties: partyResendRequests,
|
parties: partyResendRequests,
|
||||||
requestedAt: now,
|
requestedAt: now,
|
||||||
requestedByExpertId: new Types.ObjectId(actorId),
|
requestedByExpertId: new Types.ObjectId(actorId),
|
||||||
|
...(resendSnapshot && { expertProfileSnapshot: resendSnapshot }),
|
||||||
},
|
},
|
||||||
status: CaseStatus.WAITING_FOR_DOCUMENT_RESEND,
|
status: CaseStatus.WAITING_FOR_DOCUMENT_RESEND,
|
||||||
},
|
},
|
||||||
@@ -1151,12 +1204,15 @@ export class ExpertBlameService {
|
|||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
|
const expertProfileSnapshot = await this.snapshotFieldExpert(actorId);
|
||||||
|
|
||||||
// Build expert decision with fields
|
// Build expert decision with fields
|
||||||
const decisionPayload: any = {
|
const decisionPayload: any = {
|
||||||
guiltyPartyId: new Types.ObjectId(String(reply.guiltyUserId)),
|
guiltyPartyId: new Types.ObjectId(String(reply.guiltyUserId)),
|
||||||
description: reply.description,
|
description: reply.description,
|
||||||
decidedAt: now,
|
decidedAt: now,
|
||||||
decidedByExpertId: new Types.ObjectId(actorId),
|
decidedByExpertId: new Types.ObjectId(actorId),
|
||||||
|
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||||
fields: {
|
fields: {
|
||||||
accidentWay: {
|
accidentWay: {
|
||||||
id: reply.fields.accidentWay.id,
|
id: reply.fields.accidentWay.id,
|
||||||
@@ -1279,6 +1335,7 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const inPersonSnapshot = await this.snapshotFieldExpert(actorId);
|
||||||
|
|
||||||
// Build expert decision with fields
|
// Build expert decision with fields
|
||||||
const decisionPayload: any = {
|
const decisionPayload: any = {
|
||||||
@@ -1286,6 +1343,7 @@ export class ExpertBlameService {
|
|||||||
description: "حضوری مراجعه شود.",
|
description: "حضوری مراجعه شود.",
|
||||||
decidedAt: now,
|
decidedAt: now,
|
||||||
decidedByExpertId: new Types.ObjectId(actorId),
|
decidedByExpertId: new Types.ObjectId(actorId),
|
||||||
|
...(inPersonSnapshot && { expertProfileSnapshot: inPersonSnapshot }),
|
||||||
fields: {
|
fields: {
|
||||||
accidentWay: null,
|
accidentWay: null,
|
||||||
accidentReason: null,
|
accidentReason: null,
|
||||||
@@ -1445,6 +1503,8 @@ export class ExpertBlameService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const expertProfileSnapshot = await this.snapshotFieldExpert(userId);
|
||||||
|
|
||||||
const newReplyObject = {
|
const newReplyObject = {
|
||||||
description: reply.description,
|
description: reply.description,
|
||||||
submitTime: new Date(),
|
submitTime: new Date(),
|
||||||
@@ -1466,6 +1526,7 @@ export class ExpertBlameService {
|
|||||||
},
|
},
|
||||||
firstPartyComment: request.expertSubmitReply?.firstPartyComment || null,
|
firstPartyComment: request.expertSubmitReply?.firstPartyComment || null,
|
||||||
secondPartyComment: request.expertSubmitReply?.secondPartyComment || null,
|
secondPartyComment: request.expertSubmitReply?.secondPartyComment || null,
|
||||||
|
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePayload: any = {
|
const updatePayload: any = {
|
||||||
@@ -1522,6 +1583,7 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const partyType = req.route.path.split("/")[4];
|
const partyType = req.route.path.split("/")[4];
|
||||||
|
const resendSnapshot = await this.snapshotFieldExpert(userId);
|
||||||
|
|
||||||
switch (partyType) {
|
switch (partyType) {
|
||||||
case "first": {
|
case "first": {
|
||||||
@@ -1542,6 +1604,9 @@ export class ExpertBlameService {
|
|||||||
"expertResendReply.firstParty.firstPartyId": firstPartyId,
|
"expertResendReply.firstParty.firstPartyId": firstPartyId,
|
||||||
"expertResendReply.firstParty.firstPartyDescription":
|
"expertResendReply.firstParty.firstPartyDescription":
|
||||||
firstPartyDescription,
|
firstPartyDescription,
|
||||||
|
...(resendSnapshot && {
|
||||||
|
expertResendByExpertProfileSnapshot: resendSnapshot,
|
||||||
|
}),
|
||||||
$push: {
|
$push: {
|
||||||
actorsChecker: {
|
actorsChecker: {
|
||||||
[ReqBlameStatus.UserPending]: request.actorLocked,
|
[ReqBlameStatus.UserPending]: request.actorLocked,
|
||||||
@@ -1579,6 +1644,9 @@ export class ExpertBlameService {
|
|||||||
"expertResendReply.secondParty.secondPartyId": secondPartyId,
|
"expertResendReply.secondParty.secondPartyId": secondPartyId,
|
||||||
"expertResendReply.secondParty.secondPartyDescription":
|
"expertResendReply.secondParty.secondPartyDescription":
|
||||||
secondPartyDescription,
|
secondPartyDescription,
|
||||||
|
...(resendSnapshot && {
|
||||||
|
expertResendByExpertProfileSnapshot: resendSnapshot,
|
||||||
|
}),
|
||||||
$push: {
|
$push: {
|
||||||
actorsChecker: {
|
actorsChecker: {
|
||||||
[ReqBlameStatus.UserPending]: request.actorLocked,
|
[ReqBlameStatus.UserPending]: request.actorLocked,
|
||||||
@@ -1658,10 +1726,15 @@ export class ExpertBlameService {
|
|||||||
throw new NotFoundException("Blame not found");
|
throw new NotFoundException("Blame not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const visitSnapshot = await this.snapshotFieldExpert(actorDetail.sub);
|
||||||
|
|
||||||
const updated = await this.requestManagementDbService.findAndUpdate(
|
const updated = await this.requestManagementDbService.findAndUpdate(
|
||||||
{ _id: new Types.ObjectId(requestId) },
|
{ _id: new Types.ObjectId(requestId) },
|
||||||
{
|
{
|
||||||
blameStatus: ReqBlameStatus.InPersonVisit,
|
blameStatus: ReqBlameStatus.InPersonVisit,
|
||||||
|
...(visitSnapshot && {
|
||||||
|
fieldExpertInPersonVisitProfileSnapshot: visitSnapshot,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Put,
|
Put,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiBearerAuth, ApiBody, ApiParam, ApiTags } from "@nestjs/swagger";
|
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger";
|
||||||
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
import { Roles } from "src/decorators/roles.decorator";
|
import { Roles } from "src/decorators/roles.decorator";
|
||||||
@@ -38,6 +38,23 @@ export class ExpertBlameV2Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("report/status-counts")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Count blame cases by grouped status bucket (tenant)",
|
||||||
|
description:
|
||||||
|
"IN_PROGRESS groups OPEN and WAITING_FOR_SECOND_PARTY. WAITING_FOR_EXPERT, WAITING_FOR_DOCUMENT_RESEND, WAITING_FOR_SIGNATURES, and terminal statuses are counted separately. Expert-initiated files count only for the initiating expert.",
|
||||||
|
})
|
||||||
|
async getStatusReportBucketsV2(@CurrentUser() actor: any) {
|
||||||
|
try {
|
||||||
|
return await this.expertBlameService.getStatusReportBucketsV2(actor);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to load blame status report",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get(":id")
|
@Get(":id")
|
||||||
@ApiParam({ name: "id", description: "Blame case request id" })
|
@ApiParam({ name: "id", description: "Blame case request id" })
|
||||||
async findOne(@Param("id") id: string, @CurrentUser() actor: any) {
|
async findOne(@Param("id") id: string, @CurrentUser() actor: any) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { ClaimRequiredDocumentType } from 'src/Types&Enums/claim-request-management/required-document-type.enum';
|
import { ClaimRequiredDocumentType } from 'src/Types&Enums/claim-request-management/required-document-type.enum';
|
||||||
import { DamagedPartItem } from 'src/claim-request-management/dto/capture-requirements-v2.dto';
|
import { DamagedPartItem } from 'src/claim-request-management/dto/capture-requirements-v2.dto';
|
||||||
|
import { DaghiOption } from 'src/Types&Enums/claim-request-management/daghi-option.enum';
|
||||||
|
|
||||||
export class CarPartDamageV2Dto {
|
export class CarPartDamageV2Dto {
|
||||||
@ApiProperty({ example: 'left' })
|
@ApiProperty({ example: 'left' })
|
||||||
@@ -22,6 +23,31 @@ export class CarPartDamageV2Dto {
|
|||||||
part: string;
|
part: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Same shape as V1 {@link PartsList} `daghi` (damage expert reply). */
|
||||||
|
export class DaghiDetailsV2Dto {
|
||||||
|
@ApiProperty({
|
||||||
|
enum: DaghiOption,
|
||||||
|
description:
|
||||||
|
'Daghi option: ارزش لوازم بازیافتی, تحویل داغی, فاقد ارزش, با احتساب داغی',
|
||||||
|
})
|
||||||
|
@IsEnum(DaghiOption)
|
||||||
|
option: DaghiOption;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: `Required when option is '${DaghiOption.RECYCLED_PARTS_VALUE}'`,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
price?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: `Required when option is '${DaghiOption.DELIVER_DAMAGED_PART}' (Mongo ObjectId string)`,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
branchId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class PartPricingV2Dto {
|
export class PartPricingV2Dto {
|
||||||
@ApiProperty({ example: 'part-001' })
|
@ApiProperty({ example: 'part-001' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -49,10 +75,10 @@ export class PartPricingV2Dto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
totalPayment: string;
|
totalPayment: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: '500000' })
|
@ApiProperty({ type: DaghiDetailsV2Dto })
|
||||||
@IsOptional()
|
@ValidateNested()
|
||||||
@IsString()
|
@Type(() => DaghiDetailsV2Dto)
|
||||||
daghi?: string;
|
daghi: DaghiDetailsV2Dto;
|
||||||
|
|
||||||
@ApiProperty({ example: false })
|
@ApiProperty({ example: false })
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ import {
|
|||||||
claimCaseTouchesClient,
|
claimCaseTouchesClient,
|
||||||
requireActorClientKey,
|
requireActorClientKey,
|
||||||
} from "src/helpers/tenant-scope";
|
} from "src/helpers/tenant-scope";
|
||||||
|
import {
|
||||||
|
claimCaseStatusToReportBucket,
|
||||||
|
initialClaimExpertReportBuckets,
|
||||||
|
} from "src/helpers/expert-panel-status-report";
|
||||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||||
@@ -59,6 +63,8 @@ import {
|
|||||||
enrichBlamePartiesForAgreementView,
|
enrichBlamePartiesForAgreementView,
|
||||||
} from "src/helpers/blame-party-agreement-decision";
|
} from "src/helpers/blame-party-agreement-decision";
|
||||||
import { resendRequestHasPayload } from "src/helpers/claim-expert-resend";
|
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";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExpertClaimService {
|
export class ExpertClaimService {
|
||||||
@@ -183,6 +189,12 @@ export class ExpertClaimService {
|
|||||||
private readonly branchDbService: BranchDbService,
|
private readonly branchDbService: BranchDbService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
/** Load immutable profile fields from `damage-expert` for the acting expert. */
|
||||||
|
private async snapshotDamageExpert(actorId: string) {
|
||||||
|
const doc = await this.damageExpertDbService.findById(actorId);
|
||||||
|
return snapshotFromDamageExpert(doc as DamageExpertModel);
|
||||||
|
}
|
||||||
|
|
||||||
/** Map blame party `Vehicle` (name/model/type) to expert panel shape. */
|
/** Map blame party `Vehicle` (name/model/type) to expert panel shape. */
|
||||||
private expertVehicleFromPartyVehicle(v: any): {
|
private expertVehicleFromPartyVehicle(v: any): {
|
||||||
carName?: string;
|
carName?: string;
|
||||||
@@ -474,6 +486,7 @@ export class ExpertClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
||||||
|
const lockSnapshot = await this.snapshotDamageExpert(actorDetail.sub);
|
||||||
|
|
||||||
await this.claimRequestManagementDbService.findOneAndUpdate(
|
await this.claimRequestManagementDbService.findOneAndUpdate(
|
||||||
new Types.ObjectId(requestId),
|
new Types.ObjectId(requestId),
|
||||||
@@ -486,6 +499,7 @@ export class ExpertClaimService {
|
|||||||
actorLocked: {
|
actorLocked: {
|
||||||
actorId: new Types.ObjectId(actorDetail.sub),
|
actorId: new Types.ObjectId(actorDetail.sub),
|
||||||
fullName: actorDetail.fullName,
|
fullName: actorDetail.fullName,
|
||||||
|
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
$push: {
|
$push: {
|
||||||
@@ -535,6 +549,48 @@ export class ExpertClaimService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Same rules as V1 `submitReplyRequest`: daghi option + conditional price / branchId; `branchId` stored as ObjectId. */
|
||||||
|
private validateAndNormalizeDaghiForExpertReplyV2(
|
||||||
|
parts: import("./dto/expert-claim-v2.dto").PartPricingV2Dto[],
|
||||||
|
) {
|
||||||
|
for (const part of parts) {
|
||||||
|
if (!part.daghi || !part.daghi.option) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Daghi option is required for part ${part.partId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (part.daghi.option === DaghiOption.RECYCLED_PARTS_VALUE) {
|
||||||
|
if (!part.daghi.price) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Price is required for part ${part.partId} when option is '${DaghiOption.RECYCLED_PARTS_VALUE}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (part.daghi.option === DaghiOption.DELIVER_DAMAGED_PART) {
|
||||||
|
if (!part.daghi.branchId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Branch ID is required for part ${part.partId} when option is '${DaghiOption.DELIVER_DAMAGED_PART}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!Types.ObjectId.isValid(part.daghi.branchId)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Invalid branch ID format for part ${part.partId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.map((part) => ({
|
||||||
|
...part,
|
||||||
|
daghi: {
|
||||||
|
option: part.daghi.option,
|
||||||
|
...(part.daghi.price && { price: part.daghi.price }),
|
||||||
|
...(part.daghi.branchId && {
|
||||||
|
branchId: new Types.ObjectId(part.daghi.branchId),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
private findClosestCar(input: string, cars: { carName: string }[]) {
|
private findClosestCar(input: string, cars: { carName: string }[]) {
|
||||||
if (!input || !cars || cars.length === 0) {
|
if (!input || !cars || cars.length === 0) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
@@ -1298,6 +1354,8 @@ export class ExpertClaimService {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const expertProfileSnapshot = await this.snapshotDamageExpert(userId.sub);
|
||||||
|
|
||||||
const replyPayload = {
|
const replyPayload = {
|
||||||
description: reply.description,
|
description: reply.description,
|
||||||
parts: processedParts,
|
parts: processedParts,
|
||||||
@@ -1306,6 +1364,7 @@ export class ExpertClaimService {
|
|||||||
actorName: userId.fullName,
|
actorName: userId.fullName,
|
||||||
actorId: userId.sub,
|
actorId: userId.sub,
|
||||||
},
|
},
|
||||||
|
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePayload = {
|
const updatePayload = {
|
||||||
@@ -1378,6 +1437,8 @@ export class ExpertClaimService {
|
|||||||
if (request.damageExpertReplyFinal)
|
if (request.damageExpertReplyFinal)
|
||||||
throw new ForbiddenException("request already has final reply");
|
throw new ForbiddenException("request already has final reply");
|
||||||
|
|
||||||
|
const resendSnapshot = await this.snapshotDamageExpert(userId);
|
||||||
|
|
||||||
await this.claimRequestManagementDbService.findAndUpdate(requestId, {
|
await this.claimRequestManagementDbService.findAndUpdate(requestId, {
|
||||||
lockFile: false,
|
lockFile: false,
|
||||||
claimStatus: ReqClaimStatus.WaitingForUserToResend,
|
claimStatus: ReqClaimStatus.WaitingForUserToResend,
|
||||||
@@ -1385,6 +1446,7 @@ export class ExpertClaimService {
|
|||||||
resendDescription: body.resendDescription,
|
resendDescription: body.resendDescription,
|
||||||
resendDocuments: body.resendDocuments,
|
resendDocuments: body.resendDocuments,
|
||||||
resendCarParts: body.resendCarParts,
|
resendCarParts: body.resendCarParts,
|
||||||
|
...(resendSnapshot && { expertProfileSnapshot: resendSnapshot }),
|
||||||
},
|
},
|
||||||
$push: {
|
$push: {
|
||||||
actorsChecker: {
|
actorsChecker: {
|
||||||
@@ -1668,6 +1730,16 @@ export class ExpertClaimService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const factorSnap = await this.snapshotDamageExpert(expertId);
|
||||||
|
if (factorSnap && decisions.length > 0) {
|
||||||
|
await this.claimRequestManagementDbService.findOneAndUpdate(
|
||||||
|
{ _id: new Types.ObjectId(claimId) },
|
||||||
|
{
|
||||||
|
$set: { factorValidationExpertProfileSnapshot: factorSnap },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const updatedClaim =
|
const updatedClaim =
|
||||||
await this.claimRequestManagementDbService.findOne(claimId);
|
await this.claimRequestManagementDbService.findOne(claimId);
|
||||||
const updatedReply =
|
const updatedReply =
|
||||||
@@ -1748,6 +1820,8 @@ export class ExpertClaimService {
|
|||||||
|
|
||||||
assertClaimCaseForTenant(claim, actor);
|
assertClaimCaseForTenant(claim, actor);
|
||||||
|
|
||||||
|
const factorValidationSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
claim.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL ||
|
claim.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL ||
|
||||||
claim.claimStatus !== ClaimStatus.UNDER_REVIEW ||
|
claim.claimStatus !== ClaimStatus.UNDER_REVIEW ||
|
||||||
@@ -1828,6 +1902,11 @@ export class ExpertClaimService {
|
|||||||
throw new BadRequestException("No valid factor decisions to apply.");
|
throw new BadRequestException("No valid factor decisions to apply.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (factorValidationSnapshot) {
|
||||||
|
$set["evaluation.factorValidationExpertProfileSnapshot"] =
|
||||||
|
factorValidationSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
$set,
|
$set,
|
||||||
});
|
});
|
||||||
@@ -1941,10 +2020,15 @@ export class ExpertClaimService {
|
|||||||
throw new NotFoundException("Blame not found");
|
throw new NotFoundException("Blame not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const visitSnapshot = await this.snapshotDamageExpert(actorDetail.sub);
|
||||||
|
|
||||||
const updated = await this.claimRequestManagementDbService.findAndUpdate(
|
const updated = await this.claimRequestManagementDbService.findAndUpdate(
|
||||||
requestId,
|
requestId,
|
||||||
{
|
{
|
||||||
claimStatus: ReqClaimStatus.InPersonVisit,
|
claimStatus: ReqClaimStatus.InPersonVisit,
|
||||||
|
...(visitSnapshot && {
|
||||||
|
damageExpertInPersonVisitProfileSnapshot: visitSnapshot,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2013,6 +2097,8 @@ export class ExpertClaimService {
|
|||||||
return { claimRequestId, locked: true, message: 'Already locked by you' };
|
return { claimRequestId, locked: true, message: 'Already locked by you' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lockSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||||
|
|
||||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
||||||
claimStatus: ClaimStatus.UNDER_REVIEW,
|
claimStatus: ClaimStatus.UNDER_REVIEW,
|
||||||
@@ -2022,6 +2108,7 @@ export class ExpertClaimService {
|
|||||||
actorId: new Types.ObjectId(actor.sub),
|
actorId: new Types.ObjectId(actor.sub),
|
||||||
actorName: actor.fullName,
|
actorName: actor.fullName,
|
||||||
actorRole: 'damage_expert',
|
actorRole: 'damage_expert',
|
||||||
|
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||||
},
|
},
|
||||||
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||||
$push: {
|
$push: {
|
||||||
@@ -2095,6 +2182,8 @@ export class ExpertClaimService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resendSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||||
|
|
||||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
$set: {
|
$set: {
|
||||||
status: ClaimCaseStatus.WAITING_FOR_USER_RESEND,
|
status: ClaimCaseStatus.WAITING_FOR_USER_RESEND,
|
||||||
@@ -2106,6 +2195,7 @@ export class ExpertClaimService {
|
|||||||
resendDescription: desc || undefined,
|
resendDescription: desc || undefined,
|
||||||
resendDocuments: docs,
|
resendDocuments: docs,
|
||||||
resendCarParts: parts,
|
resendCarParts: parts,
|
||||||
|
...(resendSnapshot && { expertProfileSnapshot: resendSnapshot }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
$unset: {
|
$unset: {
|
||||||
@@ -2148,6 +2238,7 @@ export class ExpertClaimService {
|
|||||||
* - Must be locked by this expert (workflow.lockedBy.actorId === actor.sub)
|
* - Must be locked by this expert (workflow.lockedBy.actorId === actor.sub)
|
||||||
* - Must be in EXPERT_REVIEWING status
|
* - Must be in EXPERT_REVIEWING status
|
||||||
* - Total payment across all parts must not exceed 30,000,000
|
* - Total payment across all parts must not exceed 30,000,000
|
||||||
|
* - Each part must include `daghi` (option + conditional price/branchId) like V1
|
||||||
*
|
*
|
||||||
* On success:
|
* On success:
|
||||||
* - Stores reply in evaluation.damageExpertReply
|
* - Stores reply in evaluation.damageExpertReply
|
||||||
@@ -2201,6 +2292,11 @@ export class ExpertClaimService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const processedParts =
|
||||||
|
reply.parts?.length > 0
|
||||||
|
? this.validateAndNormalizeDaghiForExpertReplyV2(reply.parts)
|
||||||
|
: [];
|
||||||
|
|
||||||
const needsFactorUpload = reply.parts?.some((p) => p.factorNeeded === true) ?? false;
|
const needsFactorUpload = reply.parts?.some((p) => p.factorNeeded === true) ?? false;
|
||||||
|
|
||||||
const objectionSubmitted = !!claim.evaluation?.objection?.submittedAt;
|
const objectionSubmitted = !!claim.evaluation?.objection?.submittedAt;
|
||||||
@@ -2220,14 +2316,17 @@ export class ExpertClaimService {
|
|||||||
? ClaimWorkflowStep.EXPERT_FINAL_REPLY
|
? ClaimWorkflowStep.EXPERT_FINAL_REPLY
|
||||||
: ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
: ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
||||||
|
|
||||||
|
const expertProfileSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||||
|
|
||||||
const replyPayload = {
|
const replyPayload = {
|
||||||
description: reply.description,
|
description: reply.description,
|
||||||
parts: reply.parts,
|
parts: processedParts,
|
||||||
submittedAt: new Date(),
|
submittedAt: new Date(),
|
||||||
actorDetail: {
|
actorDetail: {
|
||||||
actorId: actor.sub,
|
actorId: actor.sub,
|
||||||
actorName: actor.fullName,
|
actorName: actor.fullName,
|
||||||
},
|
},
|
||||||
|
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextStep = needsFactorUpload
|
const nextStep = needsFactorUpload
|
||||||
@@ -2262,7 +2361,7 @@ export class ExpertClaimService {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
metadata: {
|
metadata: {
|
||||||
factorNeeded: needsFactorUpload,
|
factorNeeded: needsFactorUpload,
|
||||||
partsCount: reply.parts?.length ?? 0,
|
partsCount: processedParts.length,
|
||||||
isFinalReplyAfterObjection,
|
isFinalReplyAfterObjection,
|
||||||
replyField,
|
replyField,
|
||||||
},
|
},
|
||||||
@@ -2325,10 +2424,15 @@ export class ExpertClaimService {
|
|||||||
throw new ForbiddenException('This claim is locked by another expert');
|
throw new ForbiddenException('This claim is locked by another expert');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const visitSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||||
|
|
||||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
claimStatus: ClaimStatus.NEEDS_REVISION,
|
claimStatus: ClaimStatus.NEEDS_REVISION,
|
||||||
'workflow.locked': false,
|
'workflow.locked': false,
|
||||||
...(note ? { 'evaluation.visitLocation': note } : {}),
|
...(note ? { 'evaluation.visitLocation': note } : {}),
|
||||||
|
...(visitSnapshot && {
|
||||||
|
'evaluation.inPersonVisitExpertProfileSnapshot': visitSnapshot,
|
||||||
|
}),
|
||||||
$push: {
|
$push: {
|
||||||
history: {
|
history: {
|
||||||
event: 'IN_PERSON_VISIT_REQUESTED',
|
event: 'IN_PERSON_VISIT_REQUESTED',
|
||||||
@@ -2348,6 +2452,25 @@ export class ExpertClaimService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Count claim cases for this damage expert’s tenant, grouped for dashboard:
|
||||||
|
* `IN_PROGRESS` = user flow before submission complete (CREATED … CAPTURING_PART_DAMAGES);
|
||||||
|
* other {@link ClaimCaseStatus} keys unchanged.
|
||||||
|
*/
|
||||||
|
async getStatusReportBucketsV2(actor: any): Promise<Record<string, number>> {
|
||||||
|
const clientKey = requireActorClientKey(actor);
|
||||||
|
const rows = await this.claimCaseDbService.find({}, { lean: true });
|
||||||
|
const buckets = initialClaimExpertReportBuckets();
|
||||||
|
for (const doc of rows as Record<string, unknown>[]) {
|
||||||
|
if (!claimCaseTouchesClient(doc, clientKey)) continue;
|
||||||
|
const st = String(doc.status ?? "");
|
||||||
|
const key = claimCaseStatusToReportBucket(st);
|
||||||
|
buckets.all++;
|
||||||
|
buckets[key] = (buckets[key] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
return buckets;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V2: Get claim list for damage expert
|
* V2: Get claim list for damage expert
|
||||||
*
|
*
|
||||||
@@ -2815,6 +2938,8 @@ export class ExpertClaimService {
|
|||||||
throw new ForbiddenException("This claim is locked by another expert");
|
throw new ForbiddenException("This claim is locked by another expert");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const damagedPartsEditSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||||
|
|
||||||
const previous = Array.isArray((claim as any).damage?.selectedParts)
|
const previous = Array.isArray((claim as any).damage?.selectedParts)
|
||||||
? [...(claim as any).damage.selectedParts]
|
? [...(claim as any).damage.selectedParts]
|
||||||
: [];
|
: [];
|
||||||
@@ -2848,6 +2973,9 @@ export class ExpertClaimService {
|
|||||||
metadata: {
|
metadata: {
|
||||||
previousSelectedParts: previous,
|
previousSelectedParts: previous,
|
||||||
selectedParts,
|
selectedParts,
|
||||||
|
...(damagedPartsEditSnapshot && {
|
||||||
|
expertProfileSnapshot: damagedPartsEditSnapshot,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,16 @@ class InPersonVisitV2Dto {
|
|||||||
export class ExpertClaimV2Controller {
|
export class ExpertClaimV2Controller {
|
||||||
constructor(private readonly expertClaimService: ExpertClaimService) { }
|
constructor(private readonly expertClaimService: ExpertClaimService) { }
|
||||||
|
|
||||||
|
@Get("report/status-counts")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Count claim cases by grouped status bucket (tenant)",
|
||||||
|
description:
|
||||||
|
"IN_PROGRESS groups user-phase statuses before submission is complete (CREATED through CAPTURING_PART_DAMAGES). Other keys match ClaimCaseStatus. Scoped to the insurer in the JWT.",
|
||||||
|
})
|
||||||
|
async getStatusReportBucketsV2(@CurrentUser() actor: any) {
|
||||||
|
return await this.expertClaimService.getStatusReportBucketsV2(actor);
|
||||||
|
}
|
||||||
|
|
||||||
@Get("requests")
|
@Get("requests")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "List available claim requests for damage expert",
|
summary: "List available claim requests for damage expert",
|
||||||
@@ -76,7 +86,7 @@ export class ExpertClaimV2Controller {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Submit expert damage assessment reply",
|
summary: "Submit expert damage assessment reply",
|
||||||
description:
|
description:
|
||||||
"Claim must be locked by this expert (status EXPERT_REVIEWING). Submitting unlocks the claim. If any part has factorNeeded=true the claim moves to EXPERT_COST_EVALUATION; otherwise moves to INSURER_REVIEW. Total payment cap is 30,000,000.",
|
"Claim must be locked by this expert (status EXPERT_REVIEWING). Submitting unlocks the claim. Each part requires `daghi` with the same rules as V1 (DaghiOption, price for recycled value, branchId for deliver damaged part). If any part has factorNeeded=true the claim moves to EXPERT_COST_EVALUATION; otherwise moves to INSURER_REVIEW. Total payment cap is 30,000,000.",
|
||||||
})
|
})
|
||||||
@ApiParam({ name: "claimRequestId" })
|
@ApiParam({ name: "claimRequestId" })
|
||||||
@ApiBody({ type: SubmitExpertReplyV2Dto })
|
@ApiBody({ type: SubmitExpertReplyV2Dto })
|
||||||
|
|||||||
49
src/helpers/expert-panel-status-report.ts
Normal file
49
src/helpers/expert-panel-status-report.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||||
|
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||||
|
|
||||||
|
/** Blame: early workflow before expert queue / resend / signatures / terminals. */
|
||||||
|
const BLAME_IN_PROGRESS_STATUSES = new Set<string>([
|
||||||
|
CaseStatus.OPEN,
|
||||||
|
CaseStatus.WAITING_FOR_SECOND_PARTY,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claim: user flow before submission is complete (analog to `ClaimWorkflowStep.USER_SUBMISSION_COMPLETE`).
|
||||||
|
*/
|
||||||
|
const CLAIM_IN_PROGRESS_STATUSES = new Set<string>([
|
||||||
|
ClaimCaseStatus.CREATED,
|
||||||
|
ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||||
|
ClaimCaseStatus.SELECTING_OTHER_PARTS,
|
||||||
|
ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
||||||
|
ClaimCaseStatus.CAPTURING_PART_DAMAGES,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function blameCaseStatusToReportBucket(status: string): string {
|
||||||
|
if (BLAME_IN_PROGRESS_STATUSES.has(status)) return "IN_PROGRESS";
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function claimCaseStatusToReportBucket(status: string): string {
|
||||||
|
if (CLAIM_IN_PROGRESS_STATUSES.has(status)) return "IN_PROGRESS";
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keys returned for blame V2 expert report (IN_PROGRESS + each native post-early status). */
|
||||||
|
export function initialBlameExpertReportBuckets(): Record<string, number> {
|
||||||
|
const out: Record<string, number> = { all: 0, IN_PROGRESS: 0 };
|
||||||
|
for (const s of Object.values(CaseStatus)) {
|
||||||
|
if (BLAME_IN_PROGRESS_STATUSES.has(s)) continue;
|
||||||
|
out[s] = 0;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keys returned for claim V2 expert report. */
|
||||||
|
export function initialClaimExpertReportBuckets(): Record<string, number> {
|
||||||
|
const out: Record<string, number> = { all: 0, IN_PROGRESS: 0 };
|
||||||
|
for (const s of Object.values(ClaimCaseStatus)) {
|
||||||
|
if (CLAIM_IN_PROGRESS_STATUSES.has(s)) continue;
|
||||||
|
out[s] = 0;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
29
src/helpers/expert-profile-snapshot.ts
Normal file
29
src/helpers/expert-profile-snapshot.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { ExpertModel } from "src/users/entities/schema/expert.schema";
|
||||||
|
import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema";
|
||||||
|
import { ExpertProfileSnapshot } from "src/request-management/entities/schema/expert-profile-snapshot.schema";
|
||||||
|
|
||||||
|
export function snapshotFromFieldExpert(
|
||||||
|
doc: ExpertModel | null | undefined,
|
||||||
|
): ExpertProfileSnapshot | undefined {
|
||||||
|
if (!doc) return undefined;
|
||||||
|
return {
|
||||||
|
firstName: doc.firstName ?? undefined,
|
||||||
|
lastName: doc.lastName ?? undefined,
|
||||||
|
email: doc.email ?? undefined,
|
||||||
|
insuActivityCo: doc.insuActivityCo ?? undefined,
|
||||||
|
mobile: (doc.mobile ?? doc.phone) ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function snapshotFromDamageExpert(
|
||||||
|
doc: DamageExpertModel | null | undefined,
|
||||||
|
): ExpertProfileSnapshot | undefined {
|
||||||
|
if (!doc) return undefined;
|
||||||
|
return {
|
||||||
|
firstName: doc.firstName ?? undefined,
|
||||||
|
lastName: doc.lastName ?? undefined,
|
||||||
|
email: doc.email ?? undefined,
|
||||||
|
insuActivityCo: doc.insuActivityCo ?? undefined,
|
||||||
|
mobile: doc.mobile ?? undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable snapshot of the expert at reply time (field expert or damage expert).
|
||||||
|
* Stored on blame `expert.decision` and claim `evaluation.damageExpertReply*`.
|
||||||
|
*/
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ExpertProfileSnapshot {
|
||||||
|
@Prop()
|
||||||
|
firstName?: string;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
lastName?: string;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
insuActivityCo?: string;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
mobile?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExpertProfileSnapshotSchema =
|
||||||
|
SchemaFactory.createForClass(ExpertProfileSnapshot);
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
//! NEW
|
//! NEW
|
||||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
|
import {
|
||||||
|
ExpertProfileSnapshot,
|
||||||
|
ExpertProfileSnapshotSchema,
|
||||||
|
} from "./expert-profile-snapshot.schema";
|
||||||
|
|
||||||
|
|
||||||
@Schema({ _id: false })
|
@Schema({ _id: false })
|
||||||
@@ -52,6 +56,10 @@ export class ExpertDecision {
|
|||||||
accidentReason: { id: string; label: string; fanavaran: number };
|
accidentReason: { id: string; label: string; fanavaran: number };
|
||||||
accidentType: { id: string; label: string };
|
accidentType: { id: string; label: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Field expert profile at decision time (from `expert` collection). */
|
||||||
|
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||||
|
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||||
}
|
}
|
||||||
export const ExpertDecisionSchema = SchemaFactory.createForClass(ExpertDecision);
|
export const ExpertDecisionSchema = SchemaFactory.createForClass(ExpertDecision);
|
||||||
|
|
||||||
@@ -108,6 +116,10 @@ export class ExpertResend {
|
|||||||
@Prop({ type: Types.ObjectId })
|
@Prop({ type: Types.ObjectId })
|
||||||
requestedByExpertId?: Types.ObjectId;
|
requestedByExpertId?: Types.ObjectId;
|
||||||
|
|
||||||
|
/** Field expert profile when resend was requested (`expert` collection). */
|
||||||
|
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||||
|
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Unused for resend UI; clients use per-party requestedItems + inputKind.
|
* @deprecated Unused for resend UI; clients use per-party requestedItems + inputKind.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -249,6 +249,10 @@ export class SubmitReply {
|
|||||||
|
|
||||||
@Prop()
|
@Prop()
|
||||||
systemResponse: string;
|
systemResponse: string;
|
||||||
|
|
||||||
|
/** Field expert profile at reply time (`expert` collection). */
|
||||||
|
@Prop({ type: Object })
|
||||||
|
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpertResendParties {
|
export class ExpertResendParties {
|
||||||
@@ -271,6 +275,10 @@ export class ActorLockDetails {
|
|||||||
|
|
||||||
@Prop({ type: Types.ObjectId })
|
@Prop({ type: Types.ObjectId })
|
||||||
actorId?: Types.ObjectId;
|
actorId?: Types.ObjectId;
|
||||||
|
|
||||||
|
/** Field expert profile when lock was taken (`expert` collection). */
|
||||||
|
@Prop({ type: Object })
|
||||||
|
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CreationMethod {
|
export enum CreationMethod {
|
||||||
@@ -450,6 +458,14 @@ export class RequestManagementModel {
|
|||||||
@Prop({ type: Boolean, default: false })
|
@Prop({ type: Boolean, default: false })
|
||||||
isHandledStatsUpdated?: boolean;
|
isHandledStatsUpdated?: boolean;
|
||||||
|
|
||||||
|
/** Latest field expert profile when expert requested party resend (V1 `sendAgainRequest`). */
|
||||||
|
@Prop({ type: Object })
|
||||||
|
expertResendByExpertProfileSnapshot?: Record<string, string | undefined>;
|
||||||
|
|
||||||
|
/** Field expert profile when expert marked in-person visit (V1 `inPersonVisit`). */
|
||||||
|
@Prop({ type: Object })
|
||||||
|
fieldExpertInPersonVisitProfileSnapshot?: Record<string, string | undefined>;
|
||||||
|
|
||||||
// Expert-initiated file tracking
|
// Expert-initiated file tracking
|
||||||
@Prop({ type: Boolean, default: false })
|
@Prop({ type: Boolean, default: false })
|
||||||
expertInitiated?: boolean;
|
expertInitiated?: boolean;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { WorkflowStep } from "src/Types&Enums/blame-request-management/blameWorkflow-steps.enum";
|
import { WorkflowStep } from "src/Types&Enums/blame-request-management/blameWorkflow-steps.enum";
|
||||||
|
import {
|
||||||
|
ExpertProfileSnapshot,
|
||||||
|
ExpertProfileSnapshotSchema,
|
||||||
|
} from "./expert-profile-snapshot.schema";
|
||||||
|
|
||||||
@Schema({ _id: false })
|
@Schema({ _id: false })
|
||||||
export class ActorLock {
|
export class ActorLock {
|
||||||
@@ -13,6 +17,10 @@ export class ActorLock {
|
|||||||
|
|
||||||
@Prop({ type: String, default: "expert" })
|
@Prop({ type: String, default: "expert" })
|
||||||
actorRole: "expert" | "admin";
|
actorRole: "expert" | "admin";
|
||||||
|
|
||||||
|
/** Field expert profile when lock was taken (`expert` collection). */
|
||||||
|
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||||
|
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||||
}
|
}
|
||||||
export const ActorLockSchema = SchemaFactory.createForClass(ActorLock);
|
export const ActorLockSchema = SchemaFactory.createForClass(ActorLock);
|
||||||
|
|
||||||
|
|||||||
@@ -1154,13 +1154,42 @@ export class RequestManagementService {
|
|||||||
(party.statement as any).lightCondition = body.lightCondition;
|
(party.statement as any).lightCondition = body.lightCondition;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If second party finished description, we’re ready for expert flow.
|
const isSecondThirdParty =
|
||||||
|
stepKey === WorkflowStep.SECOND_DESCRIPTION &&
|
||||||
|
req.type === BlameRequestType.THIRD_PARTY;
|
||||||
|
let closedByMutualAgreement = false;
|
||||||
|
|
||||||
|
// THIRD_PARTY + AGREED: guilt/damage already implied by first-party confession — no field expert needed.
|
||||||
|
if (isSecondThirdParty && req.blameStatus === BlameStatus.AGREED) {
|
||||||
|
const built = buildMutualAgreementExpertDecision(
|
||||||
|
(req as any).toObject
|
||||||
|
? (req as any).toObject()
|
||||||
|
: { ...req },
|
||||||
|
);
|
||||||
|
if (built) {
|
||||||
|
if (!(req as any).expert) (req as any).expert = {};
|
||||||
|
(req as any).expert.decision = built as any;
|
||||||
|
const completed = Array.isArray(req.workflow.completedSteps)
|
||||||
|
? req.workflow.completedSteps
|
||||||
|
: [];
|
||||||
|
if (!completed.includes(stepKey)) completed.push(stepKey);
|
||||||
|
// Same as post–field-expert reply: guilt phase is satisfied without an expert; parties must still sign.
|
||||||
|
if (!completed.includes(WorkflowStep.WAITING_FOR_GUILT_DECISION)) {
|
||||||
|
completed.push(WorkflowStep.WAITING_FOR_GUILT_DECISION);
|
||||||
|
}
|
||||||
|
req.workflow.completedSteps = completed;
|
||||||
|
req.workflow.currentStep = WorkflowStep.WAITING_FOR_SIGNATURES;
|
||||||
|
req.workflow.nextStep = undefined;
|
||||||
|
req.status = CaseStatus.WAITING_FOR_SIGNATURES;
|
||||||
|
closedByMutualAgreement = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!closedByMutualAgreement) {
|
||||||
if (stepKey === WorkflowStep.SECOND_DESCRIPTION) {
|
if (stepKey === WorkflowStep.SECOND_DESCRIPTION) {
|
||||||
req.status = CaseStatus.WAITING_FOR_EXPERT;
|
req.status = CaseStatus.WAITING_FOR_EXPERT;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.advanceWorkflowToNext(req, stepKey);
|
await this.advanceWorkflowToNext(req, stepKey);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
stepKey === WorkflowStep.SECOND_DESCRIPTION &&
|
stepKey === WorkflowStep.SECOND_DESCRIPTION &&
|
||||||
req.type === BlameRequestType.THIRD_PARTY &&
|
req.type === BlameRequestType.THIRD_PARTY &&
|
||||||
@@ -1176,6 +1205,7 @@ export class RequestManagementService {
|
|||||||
(req as any).expert.decision = built as any;
|
(req as any).expert.decision = built as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.isArray(req.history)) req.history = [];
|
if (!Array.isArray(req.history)) req.history = [];
|
||||||
req.history.push({
|
req.history.push({
|
||||||
@@ -1187,7 +1217,16 @@ export class RequestManagementService {
|
|||||||
actorName: user?.fullName,
|
actorName: user?.fullName,
|
||||||
actorType: "user",
|
actorType: "user",
|
||||||
},
|
},
|
||||||
metadata: { role },
|
metadata: {
|
||||||
|
role,
|
||||||
|
...(closedByMutualAgreement
|
||||||
|
? {
|
||||||
|
mutualAgreementWithoutExpert: true,
|
||||||
|
closedWithoutExpert: true,
|
||||||
|
awaitingSignatures: true,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
await (req as any).save();
|
await (req as any).save();
|
||||||
|
|||||||
Reference in New Issue
Block a user