forked from Yara724/api
daghi + expert and damage expert name in decision +
signature required added
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/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 { UserReplyEnum } from "src/Types&Enums/claim-request-management/userReply.enum";
|
||||
|
||||
@@ -83,6 +87,10 @@ export class ClaimExpertReply {
|
||||
|
||||
@Prop({ type: ClaimUserCommentSchema })
|
||||
userComment?: ClaimUserComment;
|
||||
|
||||
/** Damage expert profile at reply time (from `damage-expert` collection). */
|
||||
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||
}
|
||||
export const ClaimExpertReplySchema =
|
||||
SchemaFactory.createForClass(ClaimExpertReply);
|
||||
@@ -160,6 +168,10 @@ export class ClaimResendRequest {
|
||||
/** Set when the owner has satisfied every requested document/part (or acknowledged description-only resend). */
|
||||
@Prop({ type: Date })
|
||||
fulfilledAt?: Date;
|
||||
|
||||
/** Damage expert profile when resend was requested (`damage-expert` collection). */
|
||||
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||
}
|
||||
export const ClaimResendRequestSchema =
|
||||
SchemaFactory.createForClass(ClaimResendRequest);
|
||||
@@ -239,6 +251,14 @@ export class ClaimEvaluation {
|
||||
|
||||
@Prop({ type: ClaimPriceDropSchema })
|
||||
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 =
|
||||
SchemaFactory.createForClass(ClaimEvaluation);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Types } from "mongoose";
|
||||
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 })
|
||||
export class ClaimActorLock {
|
||||
@@ -12,6 +16,10 @@ export class ClaimActorLock {
|
||||
|
||||
@Prop({ type: String, default: "damage_expert" })
|
||||
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);
|
||||
|
||||
|
||||
@@ -122,6 +122,10 @@ export class SubmitReply {
|
||||
|
||||
@Prop()
|
||||
userComment?: UserComment;
|
||||
|
||||
/** Damage expert profile at reply time (`damage-expert` collection). */
|
||||
@Prop({ type: Object })
|
||||
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
export class ActorLockDetails {
|
||||
@@ -130,6 +134,10 @@ export class ActorLockDetails {
|
||||
|
||||
@Prop({ type: 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 {
|
||||
@@ -149,6 +157,10 @@ export class ClaimSubmitResend {
|
||||
|
||||
@Prop({ required: true })
|
||||
resendCarParts: ResendCarPartsDto[];
|
||||
|
||||
/** Damage expert profile when resend was requested (`damage-expert` collection). */
|
||||
@Prop({ type: Object })
|
||||
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
export class UserResendDocuments {
|
||||
@@ -360,6 +372,14 @@ export class ClaimRequestManagementModel {
|
||||
@Prop({ type: UserClaimRating, required: false })
|
||||
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 })
|
||||
visitLocation?: string;
|
||||
|
||||
|
||||
@@ -39,6 +39,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 { UserSignDbService } from "src/request-management/entities/db-service/sign.db.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 {
|
||||
CheckedRequest?: {
|
||||
@@ -81,6 +83,14 @@ export class ExpertBlameService {
|
||||
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> {
|
||||
// 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
|
||||
@@ -767,6 +777,7 @@ export class ExpertBlameService {
|
||||
}
|
||||
|
||||
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
||||
const lockSnapshot = await this.snapshotFieldExpert(actorDetail.sub);
|
||||
|
||||
const updateResult = await this.requestManagementDbService.findOneAndUpdate(
|
||||
{
|
||||
@@ -782,6 +793,7 @@ export class ExpertBlameService {
|
||||
actorLocked: {
|
||||
fullName: actorDetail.fullName,
|
||||
actorId: new Types.ObjectId(actorDetail.sub),
|
||||
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||
},
|
||||
},
|
||||
$push: {
|
||||
@@ -872,6 +884,8 @@ export class ExpertBlameService {
|
||||
}
|
||||
}
|
||||
|
||||
const lockSnapshot = await this.snapshotFieldExpert(actorDetail.sub);
|
||||
|
||||
// Lock the request (either unlocked or expired lock)
|
||||
const updateResult = await this.blameRequestDbService.findByIdAndUpdate(
|
||||
requestId,
|
||||
@@ -883,6 +897,7 @@ export class ExpertBlameService {
|
||||
actorId: new Types.ObjectId(actorDetail.sub),
|
||||
actorName: actorDetail.fullName || "Unknown Expert",
|
||||
actorRole: "expert",
|
||||
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1004,6 +1019,7 @@ export class ExpertBlameService {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const resendSnapshot = await this.snapshotFieldExpert(actorId);
|
||||
|
||||
const partyResendRequests = resendDto.parties.map((party) => {
|
||||
const uid = String(party.partyId);
|
||||
@@ -1026,6 +1042,7 @@ export class ExpertBlameService {
|
||||
parties: partyResendRequests,
|
||||
requestedAt: now,
|
||||
requestedByExpertId: new Types.ObjectId(actorId),
|
||||
...(resendSnapshot && { expertProfileSnapshot: resendSnapshot }),
|
||||
},
|
||||
status: CaseStatus.WAITING_FOR_DOCUMENT_RESEND,
|
||||
},
|
||||
@@ -1151,12 +1168,15 @@ export class ExpertBlameService {
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const expertProfileSnapshot = await this.snapshotFieldExpert(actorId);
|
||||
|
||||
// Build expert decision with fields
|
||||
const decisionPayload: any = {
|
||||
guiltyPartyId: new Types.ObjectId(String(reply.guiltyUserId)),
|
||||
description: reply.description,
|
||||
decidedAt: now,
|
||||
decidedByExpertId: new Types.ObjectId(actorId),
|
||||
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||
fields: {
|
||||
accidentWay: {
|
||||
id: reply.fields.accidentWay.id,
|
||||
@@ -1279,6 +1299,7 @@ export class ExpertBlameService {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const inPersonSnapshot = await this.snapshotFieldExpert(actorId);
|
||||
|
||||
// Build expert decision with fields
|
||||
const decisionPayload: any = {
|
||||
@@ -1286,6 +1307,7 @@ export class ExpertBlameService {
|
||||
description: "حضوری مراجعه شود.",
|
||||
decidedAt: now,
|
||||
decidedByExpertId: new Types.ObjectId(actorId),
|
||||
...(inPersonSnapshot && { expertProfileSnapshot: inPersonSnapshot }),
|
||||
fields: {
|
||||
accidentWay: null,
|
||||
accidentReason: null,
|
||||
@@ -1445,6 +1467,8 @@ export class ExpertBlameService {
|
||||
);
|
||||
}
|
||||
|
||||
const expertProfileSnapshot = await this.snapshotFieldExpert(userId);
|
||||
|
||||
const newReplyObject = {
|
||||
description: reply.description,
|
||||
submitTime: new Date(),
|
||||
@@ -1466,6 +1490,7 @@ export class ExpertBlameService {
|
||||
},
|
||||
firstPartyComment: request.expertSubmitReply?.firstPartyComment || null,
|
||||
secondPartyComment: request.expertSubmitReply?.secondPartyComment || null,
|
||||
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||
};
|
||||
|
||||
const updatePayload: any = {
|
||||
@@ -1522,6 +1547,7 @@ export class ExpertBlameService {
|
||||
}
|
||||
|
||||
const partyType = req.route.path.split("/")[4];
|
||||
const resendSnapshot = await this.snapshotFieldExpert(userId);
|
||||
|
||||
switch (partyType) {
|
||||
case "first": {
|
||||
@@ -1542,6 +1568,9 @@ export class ExpertBlameService {
|
||||
"expertResendReply.firstParty.firstPartyId": firstPartyId,
|
||||
"expertResendReply.firstParty.firstPartyDescription":
|
||||
firstPartyDescription,
|
||||
...(resendSnapshot && {
|
||||
expertResendByExpertProfileSnapshot: resendSnapshot,
|
||||
}),
|
||||
$push: {
|
||||
actorsChecker: {
|
||||
[ReqBlameStatus.UserPending]: request.actorLocked,
|
||||
@@ -1579,6 +1608,9 @@ export class ExpertBlameService {
|
||||
"expertResendReply.secondParty.secondPartyId": secondPartyId,
|
||||
"expertResendReply.secondParty.secondPartyDescription":
|
||||
secondPartyDescription,
|
||||
...(resendSnapshot && {
|
||||
expertResendByExpertProfileSnapshot: resendSnapshot,
|
||||
}),
|
||||
$push: {
|
||||
actorsChecker: {
|
||||
[ReqBlameStatus.UserPending]: request.actorLocked,
|
||||
@@ -1658,10 +1690,15 @@ export class ExpertBlameService {
|
||||
throw new NotFoundException("Blame not found");
|
||||
}
|
||||
|
||||
const visitSnapshot = await this.snapshotFieldExpert(actorDetail.sub);
|
||||
|
||||
const updated = await this.requestManagementDbService.findAndUpdate(
|
||||
{ _id: new Types.ObjectId(requestId) },
|
||||
{
|
||||
blameStatus: ReqBlameStatus.InPersonVisit,
|
||||
...(visitSnapshot && {
|
||||
fieldExpertInPersonVisitProfileSnapshot: visitSnapshot,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { Type } from 'class-transformer';
|
||||
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 { DaghiOption } from 'src/Types&Enums/claim-request-management/daghi-option.enum';
|
||||
|
||||
export class CarPartDamageV2Dto {
|
||||
@ApiProperty({ example: 'left' })
|
||||
@@ -22,6 +23,31 @@ export class CarPartDamageV2Dto {
|
||||
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 {
|
||||
@ApiProperty({ example: 'part-001' })
|
||||
@IsString()
|
||||
@@ -49,10 +75,10 @@ export class PartPricingV2Dto {
|
||||
@IsString()
|
||||
totalPayment: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '500000' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
daghi?: string;
|
||||
@ApiProperty({ type: DaghiDetailsV2Dto })
|
||||
@ValidateNested()
|
||||
@Type(() => DaghiDetailsV2Dto)
|
||||
daghi: DaghiDetailsV2Dto;
|
||||
|
||||
@ApiProperty({ example: false })
|
||||
@IsBoolean()
|
||||
|
||||
@@ -59,6 +59,8 @@ import {
|
||||
enrichBlamePartiesForAgreementView,
|
||||
} from "src/helpers/blame-party-agreement-decision";
|
||||
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()
|
||||
export class ExpertClaimService {
|
||||
@@ -183,6 +185,12 @@ export class ExpertClaimService {
|
||||
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. */
|
||||
private expertVehicleFromPartyVehicle(v: any): {
|
||||
carName?: string;
|
||||
@@ -474,6 +482,7 @@ export class ExpertClaimService {
|
||||
}
|
||||
|
||||
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
||||
const lockSnapshot = await this.snapshotDamageExpert(actorDetail.sub);
|
||||
|
||||
await this.claimRequestManagementDbService.findOneAndUpdate(
|
||||
new Types.ObjectId(requestId),
|
||||
@@ -486,6 +495,7 @@ export class ExpertClaimService {
|
||||
actorLocked: {
|
||||
actorId: new Types.ObjectId(actorDetail.sub),
|
||||
fullName: actorDetail.fullName,
|
||||
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||
},
|
||||
},
|
||||
$push: {
|
||||
@@ -535,6 +545,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 }[]) {
|
||||
if (!input || !cars || cars.length === 0) {
|
||||
this.logger.debug(
|
||||
@@ -1298,6 +1350,8 @@ export class ExpertClaimService {
|
||||
},
|
||||
}));
|
||||
|
||||
const expertProfileSnapshot = await this.snapshotDamageExpert(userId.sub);
|
||||
|
||||
const replyPayload = {
|
||||
description: reply.description,
|
||||
parts: processedParts,
|
||||
@@ -1306,6 +1360,7 @@ export class ExpertClaimService {
|
||||
actorName: userId.fullName,
|
||||
actorId: userId.sub,
|
||||
},
|
||||
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||
};
|
||||
|
||||
const updatePayload = {
|
||||
@@ -1378,6 +1433,8 @@ export class ExpertClaimService {
|
||||
if (request.damageExpertReplyFinal)
|
||||
throw new ForbiddenException("request already has final reply");
|
||||
|
||||
const resendSnapshot = await this.snapshotDamageExpert(userId);
|
||||
|
||||
await this.claimRequestManagementDbService.findAndUpdate(requestId, {
|
||||
lockFile: false,
|
||||
claimStatus: ReqClaimStatus.WaitingForUserToResend,
|
||||
@@ -1385,6 +1442,7 @@ export class ExpertClaimService {
|
||||
resendDescription: body.resendDescription,
|
||||
resendDocuments: body.resendDocuments,
|
||||
resendCarParts: body.resendCarParts,
|
||||
...(resendSnapshot && { expertProfileSnapshot: resendSnapshot }),
|
||||
},
|
||||
$push: {
|
||||
actorsChecker: {
|
||||
@@ -1668,6 +1726,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 =
|
||||
await this.claimRequestManagementDbService.findOne(claimId);
|
||||
const updatedReply =
|
||||
@@ -1748,6 +1816,8 @@ export class ExpertClaimService {
|
||||
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
|
||||
const factorValidationSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
if (
|
||||
claim.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL ||
|
||||
claim.claimStatus !== ClaimStatus.UNDER_REVIEW ||
|
||||
@@ -1828,6 +1898,11 @@ export class ExpertClaimService {
|
||||
throw new BadRequestException("No valid factor decisions to apply.");
|
||||
}
|
||||
|
||||
if (factorValidationSnapshot) {
|
||||
$set["evaluation.factorValidationExpertProfileSnapshot"] =
|
||||
factorValidationSnapshot;
|
||||
}
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set,
|
||||
});
|
||||
@@ -1941,10 +2016,15 @@ export class ExpertClaimService {
|
||||
throw new NotFoundException("Blame not found");
|
||||
}
|
||||
|
||||
const visitSnapshot = await this.snapshotDamageExpert(actorDetail.sub);
|
||||
|
||||
const updated = await this.claimRequestManagementDbService.findAndUpdate(
|
||||
requestId,
|
||||
{
|
||||
claimStatus: ReqClaimStatus.InPersonVisit,
|
||||
...(visitSnapshot && {
|
||||
damageExpertInPersonVisitProfileSnapshot: visitSnapshot,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2013,6 +2093,8 @@ export class ExpertClaimService {
|
||||
return { claimRequestId, locked: true, message: 'Already locked by you' };
|
||||
}
|
||||
|
||||
const lockSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
||||
claimStatus: ClaimStatus.UNDER_REVIEW,
|
||||
@@ -2022,6 +2104,7 @@ export class ExpertClaimService {
|
||||
actorId: new Types.ObjectId(actor.sub),
|
||||
actorName: actor.fullName,
|
||||
actorRole: 'damage_expert',
|
||||
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||
},
|
||||
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||
$push: {
|
||||
@@ -2095,6 +2178,8 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
const resendSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: {
|
||||
status: ClaimCaseStatus.WAITING_FOR_USER_RESEND,
|
||||
@@ -2106,6 +2191,7 @@ export class ExpertClaimService {
|
||||
resendDescription: desc || undefined,
|
||||
resendDocuments: docs,
|
||||
resendCarParts: parts,
|
||||
...(resendSnapshot && { expertProfileSnapshot: resendSnapshot }),
|
||||
},
|
||||
},
|
||||
$unset: {
|
||||
@@ -2148,6 +2234,7 @@ export class ExpertClaimService {
|
||||
* - Must be locked by this expert (workflow.lockedBy.actorId === actor.sub)
|
||||
* - Must be in EXPERT_REVIEWING status
|
||||
* - Total payment across all parts must not exceed 30,000,000
|
||||
* - Each part must include `daghi` (option + conditional price/branchId) like V1
|
||||
*
|
||||
* On success:
|
||||
* - Stores reply in evaluation.damageExpertReply
|
||||
@@ -2201,6 +2288,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 objectionSubmitted = !!claim.evaluation?.objection?.submittedAt;
|
||||
@@ -2220,14 +2312,17 @@ export class ExpertClaimService {
|
||||
? ClaimWorkflowStep.EXPERT_FINAL_REPLY
|
||||
: ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
||||
|
||||
const expertProfileSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
const replyPayload = {
|
||||
description: reply.description,
|
||||
parts: reply.parts,
|
||||
parts: processedParts,
|
||||
submittedAt: new Date(),
|
||||
actorDetail: {
|
||||
actorId: actor.sub,
|
||||
actorName: actor.fullName,
|
||||
},
|
||||
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||
};
|
||||
|
||||
const nextStep = needsFactorUpload
|
||||
@@ -2262,7 +2357,7 @@ export class ExpertClaimService {
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
factorNeeded: needsFactorUpload,
|
||||
partsCount: reply.parts?.length ?? 0,
|
||||
partsCount: processedParts.length,
|
||||
isFinalReplyAfterObjection,
|
||||
replyField,
|
||||
},
|
||||
@@ -2325,10 +2420,15 @@ export class ExpertClaimService {
|
||||
throw new ForbiddenException('This claim is locked by another expert');
|
||||
}
|
||||
|
||||
const visitSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
claimStatus: ClaimStatus.NEEDS_REVISION,
|
||||
'workflow.locked': false,
|
||||
...(note ? { 'evaluation.visitLocation': note } : {}),
|
||||
...(visitSnapshot && {
|
||||
'evaluation.inPersonVisitExpertProfileSnapshot': visitSnapshot,
|
||||
}),
|
||||
$push: {
|
||||
history: {
|
||||
event: 'IN_PERSON_VISIT_REQUESTED',
|
||||
@@ -2815,6 +2915,8 @@ export class ExpertClaimService {
|
||||
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)
|
||||
? [...(claim as any).damage.selectedParts]
|
||||
: [];
|
||||
@@ -2848,6 +2950,9 @@ export class ExpertClaimService {
|
||||
metadata: {
|
||||
previousSelectedParts: previous,
|
||||
selectedParts,
|
||||
...(damagedPartsEditSnapshot && {
|
||||
expertProfileSnapshot: damagedPartsEditSnapshot,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export class ExpertClaimV2Controller {
|
||||
@ApiOperation({
|
||||
summary: "Submit expert damage assessment reply",
|
||||
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" })
|
||||
@ApiBody({ type: SubmitExpertReplyV2Dto })
|
||||
|
||||
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
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
ExpertProfileSnapshot,
|
||||
ExpertProfileSnapshotSchema,
|
||||
} from "./expert-profile-snapshot.schema";
|
||||
|
||||
|
||||
@Schema({ _id: false })
|
||||
@@ -52,6 +56,10 @@ export class ExpertDecision {
|
||||
accidentReason: { id: string; label: string; fanavaran: number };
|
||||
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);
|
||||
|
||||
@@ -108,6 +116,10 @@ export class ExpertResend {
|
||||
@Prop({ type: 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.
|
||||
*/
|
||||
|
||||
@@ -249,6 +249,10 @@ export class SubmitReply {
|
||||
|
||||
@Prop()
|
||||
systemResponse: string;
|
||||
|
||||
/** Field expert profile at reply time (`expert` collection). */
|
||||
@Prop({ type: Object })
|
||||
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
export class ExpertResendParties {
|
||||
@@ -271,6 +275,10 @@ export class ActorLockDetails {
|
||||
|
||||
@Prop({ type: 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 {
|
||||
@@ -450,6 +458,14 @@ export class RequestManagementModel {
|
||||
@Prop({ type: Boolean, default: false })
|
||||
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
|
||||
@Prop({ type: Boolean, default: false })
|
||||
expertInitiated?: boolean;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Types } from "mongoose";
|
||||
import { WorkflowStep } from "src/Types&Enums/blame-request-management/blameWorkflow-steps.enum";
|
||||
import {
|
||||
ExpertProfileSnapshot,
|
||||
ExpertProfileSnapshotSchema,
|
||||
} from "./expert-profile-snapshot.schema";
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ActorLock {
|
||||
@@ -13,6 +17,10 @@ export class ActorLock {
|
||||
|
||||
@Prop({ type: String, default: "expert" })
|
||||
actorRole: "expert" | "admin";
|
||||
|
||||
/** Field expert profile when lock was taken (`expert` collection). */
|
||||
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||
}
|
||||
export const ActorLockSchema = SchemaFactory.createForClass(ActorLock);
|
||||
|
||||
|
||||
@@ -1173,13 +1173,14 @@ export class RequestManagementService {
|
||||
? req.workflow.completedSteps
|
||||
: [];
|
||||
if (!completed.includes(stepKey)) completed.push(stepKey);
|
||||
if (!completed.includes(WorkflowStep.COMPLETED as any)) {
|
||||
completed.push(WorkflowStep.COMPLETED as any);
|
||||
// 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.COMPLETED;
|
||||
req.workflow.currentStep = WorkflowStep.WAITING_FOR_SIGNATURES;
|
||||
req.workflow.nextStep = undefined;
|
||||
req.status = CaseStatus.COMPLETED;
|
||||
req.status = CaseStatus.WAITING_FOR_SIGNATURES;
|
||||
closedByMutualAgreement = true;
|
||||
}
|
||||
}
|
||||
@@ -1219,7 +1220,11 @@ export class RequestManagementService {
|
||||
metadata: {
|
||||
role,
|
||||
...(closedByMutualAgreement
|
||||
? { mutualAgreementCaseComplete: true, closedWithoutExpert: true }
|
||||
? {
|
||||
mutualAgreementWithoutExpert: true,
|
||||
closedWithoutExpert: true,
|
||||
awaitingSignatures: true,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
} as any);
|
||||
|
||||
Reference in New Issue
Block a user