forked from Yara724/api
YARA-883 + Side ID fixes
This commit is contained in:
@@ -94,9 +94,10 @@ export class ClaimDetailV2ResponseDto {
|
||||
properties: {
|
||||
index: { type: 'number' },
|
||||
partId: {
|
||||
type: 'string',
|
||||
type: 'number',
|
||||
nullable: true,
|
||||
description:
|
||||
'Send this in PUT reply/submit parts[] — stable id (`id:{catalogId}` or `{side}|{name}`)',
|
||||
'Numeric catalog part id for outer parts (`null` for non-catalog lines); use in reply/submit and price-drop',
|
||||
},
|
||||
id: { type: 'number', nullable: true },
|
||||
name: { type: 'string' },
|
||||
@@ -109,7 +110,7 @@ export class ClaimDetailV2ResponseDto {
|
||||
})
|
||||
damagedParts?: Array<{
|
||||
index: number;
|
||||
partId: string;
|
||||
partId: number | null;
|
||||
id?: number | null;
|
||||
name: string;
|
||||
side: string;
|
||||
@@ -152,6 +153,15 @@ export class ClaimDetailV2ResponseDto {
|
||||
signLink?: string;
|
||||
signedAt?: Date | string;
|
||||
};
|
||||
priceDrop?: {
|
||||
total?: number;
|
||||
carPrice?: number;
|
||||
carModel?: number;
|
||||
carValue?: number[];
|
||||
sumOfSeverity?: number;
|
||||
coefficientYear?: number;
|
||||
partLines?: Array<Record<string, unknown>>;
|
||||
};
|
||||
};
|
||||
|
||||
@ApiPropertyOptional({
|
||||
|
||||
127
src/expert-claim/dto/claim-price-drop-v2.dto.ts
Normal file
127
src/expert-claim/dto/claim-price-drop-v2.dto.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import {
|
||||
IsArray,
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import type { PriceDropSeverity } from "src/helpers/claim-price-drop";
|
||||
|
||||
export class PriceDropPartSeverityV2Dto {
|
||||
@ApiProperty({
|
||||
example: 12,
|
||||
description: "From claim detail `damagedParts[].partId`",
|
||||
})
|
||||
@IsInt()
|
||||
partId: number;
|
||||
|
||||
@ApiProperty({
|
||||
enum: ["Minor", "Moderate", "Severe"],
|
||||
description: "Severity: جزئی / متوسط / شدید",
|
||||
})
|
||||
@IsIn(["Minor", "Moderate", "Severe"])
|
||||
severity: PriceDropSeverity;
|
||||
}
|
||||
|
||||
export class UpsertClaimPriceDropV2Dto {
|
||||
@ApiPropertyOptional({ example: "پژو 206" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
carName?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: "پژو 206 تیپ 5",
|
||||
description: "Display model string stored on `claim.vehicle.carModel`",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
carModel?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 450000000,
|
||||
description: "Market price of the car (Rials)",
|
||||
})
|
||||
carPrice: number | string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 1394,
|
||||
description:
|
||||
"Jalali production year (coefficient). Defaults from linked blame plate inquiry when omitted.",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
carModelYear?: number;
|
||||
|
||||
@ApiProperty({
|
||||
type: [PriceDropPartSeverityV2Dto],
|
||||
description:
|
||||
"One row per damaged part on the claim; coefficients come from the price-drop table × severity.",
|
||||
})
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PriceDropPartSeverityV2Dto)
|
||||
partSeverities: PriceDropPartSeverityV2Dto[];
|
||||
}
|
||||
|
||||
export class ClaimPriceDropContextV2Dto {
|
||||
@ApiProperty()
|
||||
claimRequestId: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
vehicle?: {
|
||||
carName?: string;
|
||||
carModel?: string;
|
||||
carType?: string;
|
||||
};
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: "Jalali year from blame plate inquiry (ModelField / ModelCii)",
|
||||
})
|
||||
suggestedCarModelYear?: number | null;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
priceDrop?: Record<string, unknown>;
|
||||
|
||||
@ApiProperty({
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
value: { type: "string" },
|
||||
label_fa: { type: "string" },
|
||||
},
|
||||
},
|
||||
})
|
||||
severityOptions: Array<{ value: string; label_fa: string }>;
|
||||
|
||||
@ApiProperty({ description: "Full coefficient table for all price-drop parts" })
|
||||
priceDropCatalog: Array<Record<string, unknown>>;
|
||||
|
||||
@ApiProperty({
|
||||
description: "Claim damaged parts with optional price-drop key mapping",
|
||||
})
|
||||
damagedParts: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export class ClaimPriceDropResultV2Dto {
|
||||
@ApiProperty()
|
||||
claimRequestId: string;
|
||||
|
||||
@ApiProperty()
|
||||
vehicle: {
|
||||
carName?: string;
|
||||
carModel?: string;
|
||||
carType?: string;
|
||||
};
|
||||
|
||||
@ApiProperty()
|
||||
priceDrop: Record<string, unknown>;
|
||||
|
||||
@ApiProperty({ type: "array" })
|
||||
partLines: Array<Record<string, unknown>>;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IsOptional,
|
||||
ValidateNested,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ClaimRequiredDocumentType } from 'src/Types&Enums/claim-request-management/required-document-type.enum';
|
||||
@@ -40,13 +41,11 @@ export class DaghiDetailsV2Dto {
|
||||
|
||||
export class PartPricingV2Dto {
|
||||
@ApiProperty({
|
||||
example: '12',
|
||||
description:
|
||||
'Stable part id from GET claim detail `damagedParts[].partId` or `selectedParts` identity (`id:{catalogId}` or `{side}|{name}`). Required.',
|
||||
example: 201,
|
||||
description: 'Numeric catalog part id from claim detail `damagedParts[].partId`.',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
partId: string;
|
||||
@IsInt()
|
||||
partId: number;
|
||||
|
||||
@ApiProperty({ example: 'Minor' })
|
||||
@IsString()
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
@@ -34,9 +35,9 @@ export class FactorValidationDto {
|
||||
|
||||
/** V2 ClaimCase: expert confirms or overrides part pricing when validating uploaded factors. */
|
||||
class FactorDecisionV2Dto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
partId: string;
|
||||
@ApiProperty({ example: 201 })
|
||||
@IsInt()
|
||||
partId: number;
|
||||
|
||||
@ApiProperty({ enum: [FactorStatus.APPROVED, FactorStatus.REJECTED] })
|
||||
@IsEnum([FactorStatus.APPROVED, FactorStatus.REJECTED])
|
||||
|
||||
@@ -50,6 +50,15 @@ import {
|
||||
expertPortfolioFileIdsFromActivityEvents,
|
||||
objectIdsFromStringSet,
|
||||
} from "src/helpers/expert-portfolio";
|
||||
import {
|
||||
buildCoefficientsFromPartSeverities,
|
||||
buildDamagedPartsPriceDropLines,
|
||||
buildPriceDropCatalogForApi,
|
||||
calculateClaimPriceDrop,
|
||||
PRICE_DROP_SEVERITY_OPTIONS,
|
||||
resolveVehicleModelYearFromBlame,
|
||||
} from "src/helpers/claim-price-drop";
|
||||
import { UpsertClaimPriceDropV2Dto } from "./dto/claim-price-drop-v2.dto";
|
||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
@@ -87,9 +96,13 @@ import {
|
||||
coerceDamagedPartsMediaToArray,
|
||||
normalizeCarPartDamageForExpertReply,
|
||||
normalizeDamageSelectedParts,
|
||||
partIdentityKey,
|
||||
partIdentityKeyFromCarPartDamage,
|
||||
resolveSelectedPartByPartId,
|
||||
catalogPartIdFromCarPartDamage,
|
||||
catalogPartIdFromSelectedPart,
|
||||
partLookupKey,
|
||||
resolvePartForExpertReply,
|
||||
sameCatalogPartId,
|
||||
sanitizeDamageSelectedPartV2,
|
||||
type DamageSelectedPartV2,
|
||||
} from "src/helpers/outer-damage-parts";
|
||||
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||
import { snapshotFromDamageExpert } from "src/helpers/expert-profile-snapshot";
|
||||
@@ -760,7 +773,7 @@ export class ExpertClaimService {
|
||||
|
||||
/** Price-cap total for one reply line: `totalPayment` if set, otherwise `price` + `salary`. */
|
||||
private claimReplyPartLineTotalForCap(part: {
|
||||
partId?: string;
|
||||
partId?: number | string;
|
||||
totalPayment?: unknown;
|
||||
price?: unknown;
|
||||
salary?: unknown;
|
||||
@@ -2123,8 +2136,8 @@ export class ExpertClaimService {
|
||||
|
||||
const $set: Record<string, unknown> = {};
|
||||
for (const decision of body.decisions) {
|
||||
const partIndex = finalReply.parts.findIndex(
|
||||
(p) => String(p.partId) === String(decision.partId),
|
||||
const partIndex = finalReply.parts.findIndex((p) =>
|
||||
sameCatalogPartId(p.partId, decision.partId),
|
||||
);
|
||||
if (partIndex === -1) {
|
||||
this.logger.warn(
|
||||
@@ -2217,7 +2230,7 @@ export class ExpertClaimService {
|
||||
const line = this.claimReplyPartLineTotalForCap(part);
|
||||
if (isNaN(line)) {
|
||||
throw new BadRequestException({
|
||||
message: `Part ${String((part as { partId?: string }).partId ?? "")}: invalid amount for price total — use totalPayment or price and salary.`,
|
||||
message: `Part ${String(part.partId ?? "")}: invalid amount for price total — use totalPayment or price and salary.`,
|
||||
error: "PRICE_CAP_ERROR",
|
||||
});
|
||||
}
|
||||
@@ -2705,23 +2718,23 @@ export class ExpertClaimService {
|
||||
carTypeSubmit,
|
||||
(claim as any)?.damage?.selectedOuterParts,
|
||||
);
|
||||
const ownerSelectedIdentitySet = new Set(
|
||||
ownerSelectedParts.map((p) => partIdentityKey(p)),
|
||||
);
|
||||
const seenIdentities = new Set<string>();
|
||||
const seenCatalogIds = new Set<number>();
|
||||
const expertAddedParts: DamageSelectedPartV2[] = [];
|
||||
|
||||
const processedParts = daghiNormalized.map((p) => {
|
||||
const selected = resolvePartForExpertReply(
|
||||
p.partId,
|
||||
ownerSelectedParts,
|
||||
carTypeSubmit,
|
||||
);
|
||||
if (!selected) {
|
||||
throw new BadRequestException(
|
||||
`Unknown partId "${p.partId}". Use numeric catalog id from claim detail damagedParts.`,
|
||||
);
|
||||
}
|
||||
|
||||
let carPartDamage: Record<string, unknown>;
|
||||
try {
|
||||
const selected = resolveSelectedPartByPartId(
|
||||
p.partId,
|
||||
ownerSelectedParts,
|
||||
);
|
||||
if (!selected) {
|
||||
throw new BadRequestException(
|
||||
`Unknown partId "${p.partId}". Use partId from claim detail damagedParts (after editing parts on the claim).`,
|
||||
);
|
||||
}
|
||||
carPartDamage = normalizeCarPartDamageForExpertReply(
|
||||
{
|
||||
id: selected.id,
|
||||
@@ -2733,7 +2746,6 @@ export class ExpertClaimService {
|
||||
carTypeSubmit,
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestException) throw err;
|
||||
throw new BadRequestException(
|
||||
`Invalid part ${p.partId}: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
@@ -2741,30 +2753,32 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
const identityKey = partIdentityKeyFromCarPartDamage(carPartDamage);
|
||||
if (!identityKey) {
|
||||
const catalogId = catalogPartIdFromCarPartDamage(carPartDamage);
|
||||
if (catalogId == null) {
|
||||
throw new BadRequestException(
|
||||
`Invalid part ${p.partId}: cannot derive a stable identity (need a catalog id or both name and side).`,
|
||||
`Invalid part ${p.partId}: a numeric catalog id is required.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
ownerSelectedIdentitySet.size > 0 &&
|
||||
!ownerSelectedIdentitySet.has(identityKey)
|
||||
) {
|
||||
if (seenCatalogIds.has(catalogId)) {
|
||||
throw new BadRequestException(
|
||||
`Part "${identityKey}" is not in the owner's selected damaged parts; only parts the user picked (or added via objection) can be priced.`,
|
||||
`Duplicate part submitted in expert reply: ${catalogId}.`,
|
||||
);
|
||||
}
|
||||
seenCatalogIds.add(catalogId);
|
||||
|
||||
if (seenIdentities.has(identityKey)) {
|
||||
throw new BadRequestException(
|
||||
`Duplicate part submitted in expert reply: ${identityKey}.`,
|
||||
);
|
||||
const wasOnClaim = ownerSelectedParts.some(
|
||||
(op) => partLookupKey(op) === partLookupKey(selected),
|
||||
);
|
||||
if (!wasOnClaim) {
|
||||
expertAddedParts.push(selected);
|
||||
}
|
||||
seenIdentities.add(identityKey);
|
||||
|
||||
return { ...p, partId: identityKey, carPartDamage };
|
||||
return {
|
||||
...p,
|
||||
partId: catalogId,
|
||||
carPartDamage,
|
||||
};
|
||||
});
|
||||
|
||||
const { mixedFactorAndPrice, allFactorLines } = classifyV2ExpertPricingParts(
|
||||
@@ -2820,9 +2834,26 @@ export class ExpertClaimService {
|
||||
}
|
||||
}
|
||||
|
||||
const mergedSelectedParts = [...ownerSelectedParts];
|
||||
if (expertAddedParts.length > 0) {
|
||||
const mergedKeys = new Set(
|
||||
mergedSelectedParts.map((sp) => partLookupKey(sp)),
|
||||
);
|
||||
for (const sp of expertAddedParts) {
|
||||
const k = partLookupKey(sp);
|
||||
if (!mergedKeys.has(k)) {
|
||||
mergedKeys.add(k);
|
||||
mergedSelectedParts.push(sp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatePayload: Record<string, unknown> = {
|
||||
status: nextCaseStatus,
|
||||
claimStatus: nextClaimStatus,
|
||||
...(expertAddedParts.length > 0
|
||||
? { "damage.selectedParts": mergedSelectedParts }
|
||||
: {}),
|
||||
'workflow.locked': false,
|
||||
$unset: {
|
||||
'workflow.lockedAt': '',
|
||||
@@ -3605,7 +3636,7 @@ export class ExpertClaimService {
|
||||
) as { url?: string; path?: string } | undefined;
|
||||
return {
|
||||
index,
|
||||
partId: partIdentityKey(sp),
|
||||
partId: catalogPartIdFromSelectedPart(sp),
|
||||
id: sp.id,
|
||||
name: sp.name,
|
||||
side: sp.side,
|
||||
@@ -3753,6 +3784,12 @@ export class ExpertClaimService {
|
||||
evaluationForApi.ownerPricedPartsApproval =
|
||||
enrichedEvaluation.ownerPricedPartsApproval;
|
||||
}
|
||||
if (
|
||||
isDamageExpertPhase &&
|
||||
enrichedEvaluation.priceDrop !== undefined
|
||||
) {
|
||||
evaluationForApi.priceDrop = enrichedEvaluation.priceDrop;
|
||||
}
|
||||
if (Object.keys(evaluationForApi).length === 0) {
|
||||
evaluationForApi = undefined;
|
||||
}
|
||||
@@ -3808,6 +3845,180 @@ export class ExpertClaimService {
|
||||
};
|
||||
}
|
||||
|
||||
/** V2 price-drop: claim locked by this expert during damage assessment. */
|
||||
private assertExpertCanEditClaimDuringReviewV2(claim: any, actor: any): void {
|
||||
requireActorClientKey(actor);
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
|
||||
throw new BadRequestException(
|
||||
`Claim must be EXPERT_REVIEWING to edit price drop. Current status: ${claim.status}`,
|
||||
);
|
||||
}
|
||||
if (!claim.workflow?.locked) {
|
||||
throw new ForbiddenException(
|
||||
"You must lock the claim before entering price-drop data",
|
||||
);
|
||||
}
|
||||
if (claim.workflow.lockedBy?.actorId?.toString() !== String(actor.sub)) {
|
||||
throw new ForbiddenException("This claim is locked by another expert");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Context for manual price-drop (vehicle, inquiry year, catalog, damaged parts).
|
||||
*/
|
||||
async getPriceDropContextV2(claimRequestId: string, actor: any) {
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!claim) {
|
||||
throw new NotFoundException("Claim request not found");
|
||||
}
|
||||
this.assertExpertCanEditClaimDuringReviewV2(claim, actor);
|
||||
|
||||
const carType = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||
const selectedNorm = normalizeDamageSelectedParts(
|
||||
claim.damage?.selectedParts,
|
||||
carType,
|
||||
(claim.damage as any)?.selectedOuterParts,
|
||||
);
|
||||
|
||||
let suggestedCarModelYear: number | null = null;
|
||||
if (claim.blameRequestId) {
|
||||
const blameRows = await this.blameRequestDbService.find(
|
||||
{ _id: new Types.ObjectId(claim.blameRequestId.toString()) },
|
||||
{
|
||||
lean: true,
|
||||
select: "parties.role parties.person.userId parties.vehicle.inquiry",
|
||||
},
|
||||
);
|
||||
const blame = blameRows[0] as Record<string, unknown> | undefined;
|
||||
suggestedCarModelYear = resolveVehicleModelYearFromBlame(
|
||||
claim,
|
||||
blame as Parameters<typeof resolveVehicleModelYearFromBlame>[1],
|
||||
);
|
||||
}
|
||||
|
||||
const existing = (claim as any).evaluation?.priceDrop;
|
||||
|
||||
return {
|
||||
claimRequestId: String(claim._id),
|
||||
vehicle: {
|
||||
carName: claim.vehicle?.carName,
|
||||
carModel: claim.vehicle?.carModel,
|
||||
carType: claim.vehicle?.carType,
|
||||
},
|
||||
suggestedCarModelYear,
|
||||
priceDrop: existing ?? null,
|
||||
severityOptions: PRICE_DROP_SEVERITY_OPTIONS,
|
||||
priceDropCatalog: buildPriceDropCatalogForApi(),
|
||||
damagedParts: buildDamagedPartsPriceDropLines(selectedNorm),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Expert enters vehicle + per-part severities; persists `vehicle` and `evaluation.priceDrop`.
|
||||
*/
|
||||
async upsertPriceDropV2(
|
||||
claimRequestId: string,
|
||||
body: UpsertClaimPriceDropV2Dto,
|
||||
actor: any,
|
||||
) {
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!claim) {
|
||||
throw new NotFoundException("Claim request not found");
|
||||
}
|
||||
this.assertExpertCanEditClaimDuringReviewV2(claim, actor);
|
||||
|
||||
const carType = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||
const selectedNorm = normalizeDamageSelectedParts(
|
||||
claim.damage?.selectedParts,
|
||||
carType,
|
||||
(claim.damage as any)?.selectedOuterParts,
|
||||
);
|
||||
|
||||
if (!body.partSeverities?.length) {
|
||||
throw new BadRequestException(
|
||||
"Provide at least one partSeverities line for a damaged part.",
|
||||
);
|
||||
}
|
||||
|
||||
let carModelYear = body.carModelYear;
|
||||
if (carModelYear == null && claim.blameRequestId) {
|
||||
const blameRows = await this.blameRequestDbService.find(
|
||||
{ _id: new Types.ObjectId(claim.blameRequestId.toString()) },
|
||||
{
|
||||
lean: true,
|
||||
select: "parties.role parties.person.userId parties.vehicle.inquiry",
|
||||
},
|
||||
);
|
||||
carModelYear =
|
||||
resolveVehicleModelYearFromBlame(
|
||||
claim,
|
||||
blameRows[0] as Parameters<typeof resolveVehicleModelYearFromBlame>[1],
|
||||
) ?? undefined;
|
||||
}
|
||||
if (carModelYear == null || !Number.isFinite(carModelYear)) {
|
||||
throw new BadRequestException(
|
||||
"carModelYear is required when it cannot be resolved from the linked blame plate inquiry.",
|
||||
);
|
||||
}
|
||||
|
||||
const { coefficients, lines, errors } = buildCoefficientsFromPartSeverities(
|
||||
selectedNorm,
|
||||
body.partSeverities.map((p) => ({
|
||||
partId: p.partId,
|
||||
severity: p.severity,
|
||||
})),
|
||||
carType,
|
||||
);
|
||||
if (errors.length > 0) {
|
||||
throw new BadRequestException(errors.join(" "));
|
||||
}
|
||||
if (coefficients.length === 0) {
|
||||
throw new BadRequestException(
|
||||
"No valid severity coefficients could be derived from partSeverities.",
|
||||
);
|
||||
}
|
||||
|
||||
const calculated = calculateClaimPriceDrop(
|
||||
body.carPrice,
|
||||
carModelYear,
|
||||
coefficients,
|
||||
);
|
||||
|
||||
const priceDropPayload = {
|
||||
...calculated,
|
||||
partLines: lines,
|
||||
};
|
||||
|
||||
const vehicleSet: Record<string, string> = {};
|
||||
if (body.carName != null && String(body.carName).trim()) {
|
||||
vehicleSet["vehicle.carName"] = String(body.carName).trim();
|
||||
}
|
||||
if (body.carModel != null && String(body.carModel).trim()) {
|
||||
vehicleSet["vehicle.carModel"] = String(body.carModel).trim();
|
||||
}
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: {
|
||||
...vehicleSet,
|
||||
"evaluation.priceDrop": priceDropPayload,
|
||||
},
|
||||
});
|
||||
|
||||
const updated = await this.claimCaseDbService.findById(claimRequestId);
|
||||
|
||||
return {
|
||||
claimRequestId,
|
||||
vehicle: {
|
||||
carName: updated?.vehicle?.carName,
|
||||
carModel: updated?.vehicle?.carModel,
|
||||
carType: updated?.vehicle?.carType,
|
||||
},
|
||||
priceDrop: priceDropPayload,
|
||||
partLines: lines,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Damage expert can modify selected damaged parts before final submit.
|
||||
* Allowed only when claim is EXPERT_REVIEWING and locked by this expert.
|
||||
@@ -3850,13 +4061,19 @@ export class ExpertClaimService {
|
||||
previousNorm,
|
||||
);
|
||||
|
||||
const nextNorm = body.selectedParts.map((p) => ({
|
||||
id: p.id ?? null,
|
||||
name: String(p.name || "").trim(),
|
||||
side: String(p.side || ""),
|
||||
label_fa: String(p.label_fa || "").trim() || String(p.name || "").trim(),
|
||||
...(p.catalogKey?.trim() ? { catalogKey: p.catalogKey.trim() } : {}),
|
||||
}));
|
||||
const nextNorm = body.selectedParts.map((p) =>
|
||||
sanitizeDamageSelectedPartV2(
|
||||
{
|
||||
id: p.id ?? null,
|
||||
name: String(p.name || "").trim(),
|
||||
side: String(p.side || ""),
|
||||
label_fa:
|
||||
String(p.label_fa || "").trim() || String(p.name || "").trim(),
|
||||
...(p.catalogKey?.trim() ? { catalogKey: p.catalogKey.trim() } : {}),
|
||||
},
|
||||
carType,
|
||||
),
|
||||
);
|
||||
|
||||
const matchPreviousIndex = (
|
||||
next: (typeof nextNorm)[0],
|
||||
|
||||
@@ -18,6 +18,11 @@ import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { ExpertClaimService } from "./expert-claim.service";
|
||||
import { ClaimSubmitResendV2Dto, SubmitExpertReplyV2Dto } from "./dto/expert-claim-v2.dto";
|
||||
import { UpdateClaimDamagedPartsV2Dto } from "./dto/update-claim-damaged-parts-v2.dto";
|
||||
import {
|
||||
ClaimPriceDropContextV2Dto,
|
||||
ClaimPriceDropResultV2Dto,
|
||||
UpsertClaimPriceDropV2Dto,
|
||||
} from "./dto/claim-price-drop-v2.dto";
|
||||
import { FactorValidationV2Dto } from "./dto/factor-validation.dto";
|
||||
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
||||
import { OuterPartCatalogItemDto } from "src/claim-request-management/dto/select-outer-parts-v2.dto";
|
||||
@@ -101,7 +106,7 @@ export class ExpertClaimV2Controller {
|
||||
@ApiOperation({
|
||||
summary: "Get claim request detail for damage expert",
|
||||
description:
|
||||
"Returns full claim details including captured images, required documents, damage selections, `videoCapture` (from claim-video-capture via media.videoCaptureId), and `blameCase` (linked blameCases document with party video/voice URLs like expert-blame detail). Allowed when status is WAITING_FOR_DAMAGE_EXPERT (if locked, only the locking expert) or when awaiting factor validation.",
|
||||
"Returns full claim details including captured images, required documents, damage selections, `evaluation.priceDrop` during damage review, `videoCapture` (from claim-video-capture via media.videoCaptureId), and `blameCase` (linked blameCases document with party video/voice URLs like expert-blame detail). Allowed when status is WAITING_FOR_DAMAGE_EXPERT (if locked, only the locking expert) or when awaiting factor validation.",
|
||||
})
|
||||
@ApiParam({ name: "claimRequestId" })
|
||||
async getClaimDetailV2(
|
||||
@@ -111,6 +116,45 @@ export class ExpertClaimV2Controller {
|
||||
return await this.expertClaimService.getClaimDetailV2(claimRequestId, actor);
|
||||
}
|
||||
|
||||
@Get("request/:claimRequestId/price-drop")
|
||||
@ApiOperation({
|
||||
summary: "Price-drop context for manual expert entry (V2)",
|
||||
description:
|
||||
"While the claim is locked in EXPERT_REVIEWING: severity labels (جزیی / متوسط / شدید), coefficient catalog, damaged parts with price-drop mapping, suggested Jalali year from linked blame plate inquiry (`ModelField` / `ModelCii`), and saved `evaluation.priceDrop` if any.",
|
||||
})
|
||||
@ApiParam({ name: "claimRequestId" })
|
||||
@ApiResponse({ status: 200, type: ClaimPriceDropContextV2Dto })
|
||||
async getPriceDropContextV2(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@CurrentUser() actor,
|
||||
) {
|
||||
return await this.expertClaimService.getPriceDropContextV2(
|
||||
claimRequestId,
|
||||
actor,
|
||||
);
|
||||
}
|
||||
|
||||
@Put("request/:claimRequestId/price-drop")
|
||||
@ApiOperation({
|
||||
summary: "Calculate and save price drop (V2)",
|
||||
description:
|
||||
"Expert supplies car market price, optional `vehicle.carName` / `vehicle.carModel`, per-damaged-part severity (`partId` from claim detail), and optional `carModelYear` (defaults from blame inquiry). Persists `evaluation.priceDrop` using `(carPrice × yearCoefficient × sumOfCoefficients) / 400`.",
|
||||
})
|
||||
@ApiParam({ name: "claimRequestId" })
|
||||
@ApiBody({ type: UpsertClaimPriceDropV2Dto })
|
||||
@ApiResponse({ status: 200, type: ClaimPriceDropResultV2Dto })
|
||||
async upsertPriceDropV2(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@Body() body: UpsertClaimPriceDropV2Dto,
|
||||
@CurrentUser() actor,
|
||||
) {
|
||||
return await this.expertClaimService.upsertPriceDropV2(
|
||||
claimRequestId,
|
||||
body,
|
||||
actor,
|
||||
);
|
||||
}
|
||||
|
||||
@Put("lock/:claimRequestId")
|
||||
@ApiOperation({
|
||||
summary: "Lock a claim request for review",
|
||||
|
||||
Reference in New Issue
Block a user