diff --git a/src/auth/auth-controllers/user/user.auth.controller.ts b/src/auth/auth-controllers/user/user.auth.controller.ts index fee6fad..79cba20 100644 --- a/src/auth/auth-controllers/user/user.auth.controller.ts +++ b/src/auth/auth-controllers/user/user.auth.controller.ts @@ -22,7 +22,7 @@ export class UserAuthController { @Post("/send-otp") @ApiBody({ type: UserLoginDto, - description: "user login api -- call this api and send otp", + description: "Users can ask for OTP via this API and receive it", }) @ApiAcceptedResponse() async sendOtpRq(@Body() body: UserLoginDto) { @@ -40,7 +40,8 @@ export class UserAuthController { @UseGuards(LocalUserAuthGuard) @ApiBody({ type: UserVerifyOtp, - description: "user verify otp -- call this api and get a tokens", + description: + "Users can send their credentials and get their access token to server", }) @ApiAcceptedResponse() async login(@Body() body, @Req() req, @CurrentUser() user) { diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 4ffb03c..11e3a12 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -143,6 +143,7 @@ import { } from "src/helpers/claim-capture-phase"; import { HttpService } from "@nestjs/axios"; import { firstValueFrom } from "rxjs"; +import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher"; @Injectable() export class ClaimRequestManagementService { @@ -4660,7 +4661,7 @@ export class ClaimRequestManagementService { "money.sheba": shebaNumber, "money.nationalCodeOfInsurer": nationalCode, - status: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, + status: ClaimCaseStatus.CAPTURING_PART_DAMAGES, "workflow.currentStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES, "workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, @@ -6074,6 +6075,9 @@ export class ClaimRequestManagementService { "workflow.nextStep": ClaimWorkflowStep.EXPERT_COST_EVALUATION, "evaluation.objection": objectionPayload, "damage.selectedParts": mergedNorm, + ...(pendingResendGate && { + "evaluation.damageExpertResend.fulfilledAt": new Date(), + }), }, $unset: { "evaluation.ownerPricedPartsApproval": "", @@ -6657,24 +6661,15 @@ export class ClaimRequestManagementService { } } - const damagedParts = displayParts.map((sp, index) => { - const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp); - const cap = getDamagedPartCaptureBlob( - damagedPartsData, - ck, - selectedNormDetails, - ) as { url?: string; path?: string; fileName?: string } | undefined; - return { - index, - id: sp.id, - name: sp.name, - side: sp.side, - label_fa: sp.label_fa, - captured: !!cap, - url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined), - ...(cap?.path ? { path: cap.path } : {}), - ...(cap?.fileName ? { fileName: cap.fileName } : {}), - }; + const damagedParts = buildEnrichedDamagedParts({ + selectedParts: displayParts, + damagedPartsData: damagedPartsData, + evaluationBlock: (claim as any).evaluation, + expertAddedParts: (claim.damage as any)?.expertAddedParts ?? [], + getDamagedPartCaptureBlob, + hasDamagedPartCapture, + catalogLikeKeyFromPart, + buildFileLink, }); const er = claim.evaluation?.damageExpertResend; diff --git a/src/helpers/claim-expert-resend.ts b/src/helpers/claim-expert-resend.ts index 3b50371..ecf700a 100644 --- a/src/helpers/claim-expert-resend.ts +++ b/src/helpers/claim-expert-resend.ts @@ -32,13 +32,17 @@ export function resendRequestHasPayload(r: { }): boolean { if (!r) return false; if (String(r.resendDescription || "").trim() !== "") return true; - if (Array.isArray(r.resendDocuments) && r.resendDocuments.length > 0) return true; - if (Array.isArray(r.resendCarParts) && r.resendCarParts.length > 0) return true; + if (Array.isArray(r.resendDocuments) && r.resendDocuments.length > 0) + return true; + if (Array.isArray(r.resendCarParts) && r.resendCarParts.length > 0) + return true; return false; } /** Normalize expert-requested document keys (strings or `{ key }`) to canonical enum strings. */ -export function normalizeResendDocumentKeys(docs: unknown[] | undefined): string[] { +export function normalizeResendDocumentKeys( + docs: unknown[] | undefined, +): string[] { if (!Array.isArray(docs)) return []; const keys: string[] = []; for (const d of docs) { @@ -61,10 +65,7 @@ export function normalizeResendPartKeys( selectedParts?: unknown, ): string[] { if (!Array.isArray(parts)) return []; - const selectedNorm = normalizeDamageSelectedParts( - selectedParts, - carType, - ); + const selectedNorm = normalizeDamageSelectedParts(selectedParts, carType); const keys: string[] = []; for (const p of parts) { const enriched = @@ -72,8 +73,7 @@ export function normalizeResendPartKeys( matchResendPartFromSelected(p, selectedNorm); if (enriched) { const ck = - enriched.catalogKey?.trim() || - catalogLikeKeyFromPart(enriched); + enriched.catalogKey?.trim() || catalogLikeKeyFromPart(enriched); if (ck) keys.push(ck); continue; } @@ -108,7 +108,10 @@ function resolveResendPartRow( const fromSelected = matchResendPartFromSelected(raw, selectedNorm ?? []); if (fromSelected) return fromSelected; if (fromRaw) return fromRaw; - const o = (raw && typeof raw === "object" ? raw : {}) as Record; + const o = (raw && typeof raw === "object" ? raw : {}) as Record< + string, + unknown + >; const fallbackKey = typeof o.key === "string" && o.key.trim() ? o.key.trim() @@ -123,8 +126,7 @@ function resolveResendPartRow( typeof o.label_fa === "string" && o.label_fa.trim() ? o.label_fa.trim() : fallbackKey, - catalogKey: - typeof o.catalogKey === "string" ? o.catalogKey : undefined, + catalogKey: typeof o.catalogKey === "string" ? o.catalogKey : undefined, }; } @@ -149,8 +151,7 @@ export function mapExpertResendCarPartsForClient( const stored = raw && typeof raw === "object" ? (raw as Record) : {}; const sp = resolveResendPartRow(raw, options.carType, selectedNorm); - const catalogKey = - sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp); + const catalogKey = sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp); const captureKey = catalogKey || partLookupKey(sp); const cap = getDamagedPartCaptureBlob( options.damagedPartsData, @@ -193,8 +194,7 @@ export function normalizeResendCarPartsForStorage( const stored = raw && typeof raw === "object" ? (raw as Record) : {}; const sp = resolveResendPartRow(raw, carType, selectedNorm); - const catalogKey = - sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp); + const catalogKey = sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp); const row: Record = { id: sp.id, name: sp.name, @@ -217,8 +217,13 @@ function getRequiredDocEntry(claim: any, documentKey: string): unknown { return rd instanceof Map ? rd.get(documentKey) : rd[documentKey]; } -export function isRequiredDocumentUploaded(claim: any, documentKey: string): boolean { - const doc = getRequiredDocEntry(claim, documentKey) as { uploaded?: boolean } | undefined; +export function isRequiredDocumentUploaded( + claim: any, + documentKey: string, +): boolean { + const doc = getRequiredDocEntry(claim, documentKey) as + | { uploaded?: boolean } + | undefined; return !!doc?.uploaded; } @@ -241,7 +246,8 @@ export function hasDamagedPartCapture(claim: any, partKey: string): boolean { export function canFinalizeExpertResend(claim: any): boolean { if (!claim) return false; if (claim.status !== ClaimCaseStatus.WAITING_FOR_USER_RESEND) return false; - if (claim.workflow?.currentStep !== ClaimWorkflowStep.USER_EXPERT_RESEND) return false; + if (claim.workflow?.currentStep !== ClaimWorkflowStep.USER_EXPERT_RESEND) + return false; const r = claim.evaluation?.damageExpertResend; if (!r || r.fulfilledAt) return false; @@ -279,16 +285,39 @@ export function documentKeyAllowedForExpertResend( return allowed.has(normalized); } -export function partKeyAllowedForExpertResend(claim: any, partKey: string): boolean { +export function partKeyAllowedForExpertResend( + claim: any, + partKey: string, +): boolean { const r = claim?.evaluation?.damageExpertResend; if (!r || r.fulfilledAt) return false; + const carType = claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined; - const allowed = new Set( - normalizeResendPartKeys( - r.resendCarParts, - carType, - claim?.damage?.selectedParts, - ), + const selectedParts = claim?.damage?.selectedParts; + + // Normalize the stored resend parts to their canonical catalog keys + const allowedKeys = new Set( + normalizeResendPartKeys(r.resendCarParts, carType, selectedParts), ); - return allowed.has(partKey); + + // Normalize the incoming partKey the same way so the comparison is apples-to-apples + const resolvedKeys = normalizeResendPartKeys( + [partKey], + carType, + selectedParts, + ); + const resolvedKey = resolvedKeys[0]; + + // Also try direct catalogKey/name match against the stored objects as fallback + const directMatch = ((r.resendCarParts as any[]) ?? []).some((p: any) => { + if (!p || typeof p !== "object") return String(p) === partKey; + return ( + p.catalogKey === partKey || + p.key === partKey || + p.name === partKey || + (p.id != null && String(p.id) === partKey) + ); + }); + + return directMatch || (resolvedKey != null && allowedKeys.has(resolvedKey)); } diff --git a/src/helpers/claim-resend-document-keys.ts b/src/helpers/claim-resend-document-keys.ts index 67739b5..eb7d8cf 100644 --- a/src/helpers/claim-resend-document-keys.ts +++ b/src/helpers/claim-resend-document-keys.ts @@ -8,20 +8,27 @@ const REQUIRED_VALUES = new Set( * Non-canonical document keys that may still appear on stored * `evaluation.damageExpertResend.resendDocuments` → {@link ClaimRequiredDocumentType}. */ -const RESEND_DOCUMENT_KEY_ALIASES: Readonly< - Record -> = { +const RESEND_DOCUMENT_KEY_ALIASES = { + // existing entries nationalCertificate: ClaimRequiredDocumentType.NATIONAL_CARD, NationalCertificate: ClaimRequiredDocumentType.NATIONAL_CARD, + national_card: ClaimRequiredDocumentType.NATIONAL_CARD, // ← add drivingLicense: ClaimRequiredDocumentType.DAMAGED_DRIVING_LICENSE_FRONT, DrivingLicense: ClaimRequiredDocumentType.DAMAGED_DRIVING_LICENSE_FRONT, + driving_license: ClaimRequiredDocumentType.DAMAGED_DRIVING_LICENSE_FRONT, // ← add carGreenCard: ClaimRequiredDocumentType.CAR_GREEN_CARD, CarGreenCard: ClaimRequiredDocumentType.CAR_GREEN_CARD, + car_green_card: ClaimRequiredDocumentType.CAR_GREEN_CARD, // ← add (redundant but safe) carCertificate: ClaimRequiredDocumentType.DAMAGED_CAR_CARD_FRONT, CarCertificate: ClaimRequiredDocumentType.DAMAGED_CAR_CARD_FRONT, + car_certificate: ClaimRequiredDocumentType.CAR_CERTIFICATE, // ← add plate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE, carPlate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE, CarPlate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE, + // capture-phase docs — already in REQUIRED_VALUES so these are just safety nets + damaged_chassis_number: ClaimRequiredDocumentType.DAMAGED_CHASSIS_NUMBER, + damaged_engine_photo: ClaimRequiredDocumentType.DAMAGED_ENGINE_PHOTO, + damaged_metal_plate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE, }; /** Normalize a single key to a {@link ClaimRequiredDocumentType} value, or null if unknown. */ diff --git a/src/helpers/outer-damage-parts.ts b/src/helpers/outer-damage-parts.ts index 2cf577d..1f97cfb 100644 --- a/src/helpers/outer-damage-parts.ts +++ b/src/helpers/outer-damage-parts.ts @@ -62,7 +62,9 @@ export function findCatalogItemByPersianLabel( /** Non-catalog lines (expert-added interior / free-text); never use catalog numeric ids. */ export function isInternalDamageSide(side: string): boolean { - const s = String(side ?? "").trim().toLowerCase(); + const s = String(side ?? "") + .trim() + .toLowerCase(); if (!s || s === "internal" || s === "other") return true; return !SIDE_SET.has(s); } @@ -120,7 +122,9 @@ const SIDE_LABEL_FA: Record = { [OuterPartSideV2.TOP]: "بالا", }; -function outerCatalogListForItem(item: OuterPartCatalogItem): OuterPartCatalogItem[] { +function outerCatalogListForItem( + item: OuterPartCatalogItem, +): OuterPartCatalogItem[] { for (const list of Object.values(OUTER_PARTS_BY_CAR_TYPE)) { if (list.some((x) => x.key === item.key || x.id === item.id)) { return list; @@ -165,9 +169,10 @@ function findCatalogItemByKey(key: string): OuterPartCatalogItem | undefined { } /** Strip leading `left_|right_|…` when present so `name` is side-agnostic for clients. */ -export function splitCatalogKeyToNameAndSide( - fullKey: string, -): { name: string; side: string } { +export function splitCatalogKeyToNameAndSide(fullKey: string): { + name: string; + side: string; +} { const t = String(fullKey ?? "").trim(); const i = t.indexOf("_"); if (i <= 0) return { name: t, side: "" }; @@ -329,7 +334,8 @@ export function normalizeDamageSelectedParts( let catItem: OuterPartCatalogItem | undefined; if (catalog && byCatalogKey) { if (ck) catItem = byCatalogKey.get(ck); - if (!catItem && id != null) catItem = catalog.find((c) => c.id === id); + if (!catItem && id != null) + catItem = catalog.find((c) => c.id === id); if (!catItem && side && name) { catItem = byCatalogKey.get(catalogLikeKeyFromPart({ side, name })); } @@ -377,7 +383,9 @@ export function normalizeDamageSelectedParts( const cat = byCatalogKey?.get(k) ?? findCatalogItemByKey(k) ?? - (toNum(o.id) != null ? CATALOG_ITEM_BY_ID.get(toNum(o.id)!) : undefined); + (toNum(o.id) != null + ? CATALOG_ITEM_BY_ID.get(toNum(o.id)!) + : undefined); if (cat) { const list = catalog ?? outerCatalogListForItem(cat); out.push(catalogItemToSelectedPart(cat, list)); @@ -581,7 +589,9 @@ export function catalogPartIdFromCarPartDamage( export const partIdentityKeyFromCarPartDamage = catalogPartIdFromCarPartDamage; function normPartSegment(s: string): string { - return String(s ?? "").replace(/[^a-z0-9]/gi, "").toLowerCase(); + return String(s ?? "") + .replace(/[^a-z0-9]/gi, "") + .toLowerCase(); } function looseCatalogNameMatch( @@ -601,9 +611,13 @@ export function findCatalogItemFromLegacyExpertPart( side: string, carType?: ClaimVehicleTypeV2, ): OuterPartCatalogItem | undefined { - const s = String(side ?? "").toLowerCase().trim(); + const s = String(side ?? "") + .toLowerCase() + .trim(); const lists = carType - ? [OUTER_PARTS_BY_CAR_TYPE[carType]].filter(Boolean) as OuterPartCatalogItem[][] + ? ([OUTER_PARTS_BY_CAR_TYPE[carType]].filter( + Boolean, + ) as OuterPartCatalogItem[][]) : (Object.values(OUTER_PARTS_BY_CAR_TYPE) as OuterPartCatalogItem[][]); for (const catalog of lists) { if (!catalog?.length) continue; @@ -615,7 +629,9 @@ export function findCatalogItemFromLegacyExpertPart( return undefined; } -function selectedPartToStoredRecord(p: DamageSelectedPartV2): Record { +function selectedPartToStoredRecord( + p: DamageSelectedPartV2, +): Record { const r: Record = { name: p.name, side: String(p.side || "").toLowerCase(), @@ -780,9 +796,7 @@ export function isDamagedPartsMediaArray(data: unknown): boolean { return Array.isArray(data); } -function getLegacyMapRecord( - data: unknown, -): Record | null { +function getLegacyMapRecord(data: unknown): Record | null { if (data == null) return null; if (data instanceof Map) { return Object.fromEntries((data as Map).entries()); @@ -819,11 +833,9 @@ export function migrateLegacyDamagedPartsMapToArray( selected: DamageSelectedPartV2[], ): Record[] { return selected.map((sp) => { - const keysTry = [ - sp.catalogKey, - catalogLikeKeyFromPart(sp), - sp.name, - ].filter(Boolean) as string[]; + const keysTry = [sp.catalogKey, catalogLikeKeyFromPart(sp), sp.name].filter( + Boolean, + ) as string[]; let blob: unknown; for (const k of keysTry) { blob = getCaptureBlobLoose(legacyMap, k); @@ -887,12 +899,14 @@ export function resolvePartCaptureIndex( if (!raw) return -1; if (selected.length) { + // Pass 1: exact matches on catalogKey and catalogLikeKey for (let i = 0; i < selected.length; i++) { const sp = selected[i]; if (sp.catalogKey === raw) return i; if (catalogLikeKeyFromPart(sp) === raw) return i; } + // Pass 2: numeric id or array index if (/^\d+$/.test(raw)) { const n = Number(raw); const byId = selected.findIndex((p) => p.id === n); @@ -901,31 +915,35 @@ export function resolvePartCaptureIndex( if (!hasIdCollision && n >= 0 && n < selected.length) return n; } + // Pass 3: loose normalized match (handles spaces, underscores, case) const loose = raw.toLowerCase().replace(/[\s_-]/g, ""); for (let i = 0; i < selected.length; i++) { const sp = selected[i]; - const cand = [ + const candidates = [ sp.catalogKey, catalogLikeKeyFromPart(sp), sp.name, ].filter(Boolean) as string[]; - for (const c of cand) { + for (const c of candidates) { if (c.toLowerCase().replace(/[\s_-]/g, "") === loose) return i; } } - } - if (damagedPartsArray && damagedPartsArray.length) { - for (let i = 0; i < damagedPartsArray.length; i++) { - const row = damagedPartsArray[i]; - if (!row || typeof row !== "object") continue; - const id = toNum((row as { id?: unknown }).id); - if (id != null && String(id) === raw) return i; - const ck = String((row as { catalogKey?: unknown }).catalogKey || ""); - if (ck && ck === raw) return i; - const name = String((row as { name?: unknown }).name || ""); - const side = String((row as { side?: unknown }).side || ""); - if (side && name && `${side}_${name}` === raw) return i; + // Pass 4: suffix match — handles car-type-prefixed catalog keys + // e.g. "sedan_front_hood" should match sp.catalogKey="front_hood" + // or normalizeResendPartKeys output "sedan_front_hood" vs stored "front_hood" + for (let i = 0; i < selected.length; i++) { + const sp = selected[i]; + const candidates = [ + sp.catalogKey, + catalogLikeKeyFromPart(sp), + sp.name, + ].filter(Boolean) as string[]; + for (const c of candidates) { + const cLoose = c.toLowerCase().replace(/[\s_-]/g, ""); + // raw ends with the candidate (prefix stripped) + if (loose.endsWith(cLoose) || cLoose.endsWith(loose)) return i; + } } } @@ -939,7 +957,11 @@ export function mediaRowOrLegacyHasCapture( ): boolean { if (damagedPartsData == null) return false; if (Array.isArray(damagedPartsData)) { - const idx = resolvePartCaptureIndex(partKey, selected, damagedPartsData as any[]); + const idx = resolvePartCaptureIndex( + partKey, + selected, + damagedPartsData as any[], + ); if (idx < 0) return false; return mediaRowHasCapture((damagedPartsData as unknown[])[idx]); } diff --git a/src/sms-orchestration/sms-orchestration.service.ts b/src/sms-orchestration/sms-orchestration.service.ts index 65d0b5a..29247aa 100644 --- a/src/sms-orchestration/sms-orchestration.service.ts +++ b/src/sms-orchestration/sms-orchestration.service.ts @@ -41,7 +41,10 @@ export class SmsOrchestrationService implements OnModuleInit { return `${process.env.URL}/${frontendRoute}?token=${requestId}`; } - buildBlamePartyLink(requestId: string, partyRole: "FIRST" | "SECOND"): string { + buildBlamePartyLink( + requestId: string, + partyRole: "FIRST" | "SECOND", + ): string { const route = partyRole === "SECOND" ? "user2" : "user"; return `${process.env.URL}/${route}?token=${requestId}`; } @@ -50,7 +53,11 @@ export class SmsOrchestrationService implements OnModuleInit { return `${process.env.URL}/caseClaim?token=${claimRequestId}`; } - async sendInviteLink(phoneNumber: string, publicId: string, link: string): Promise { + async sendInviteLink( + phoneNumber: string, + publicId: string, + link: string, + ): Promise { return this.sendTemplate({ template: "yara724-invite-link", receptor: phoneNumber, @@ -132,14 +139,12 @@ export class SmsOrchestrationService implements OnModuleInit { }); } - async sendThirdPartyExpertStartedReviewNotice( - params: { - receptor: string; - fileKind: "blame" | "claim"; - publicId: string; - expertLastName: string; - }, - ): Promise { + async sendThirdPartyExpertStartedReviewNotice(params: { + receptor: string; + fileKind: "blame" | "claim"; + publicId: string; + expertLastName: string; + }): Promise { return this.sendTemplate({ template: "yara-expert-lock", receptor: params.receptor,