1
0
forked from Yara724/api

daghi + expert and damage expert name in decision +

signature required added
This commit is contained in:
2026-04-20 18:19:25 +03:30
parent b63c2155bc
commit 41c44dcf23
13 changed files with 324 additions and 12 deletions

View File

@@ -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()

View File

@@ -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,
}),
},
});

View File

@@ -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 })