1
0
forked from Yara724/api

Fixed damagedParts unified structure and resend problems

This commit is contained in:
SepehrYahyaee
2026-06-03 11:34:21 +03:30
parent 456135ad08
commit 077bae429e
6 changed files with 154 additions and 95 deletions

View File

@@ -22,7 +22,7 @@ export class UserAuthController {
@Post("/send-otp") @Post("/send-otp")
@ApiBody({ @ApiBody({
type: UserLoginDto, 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() @ApiAcceptedResponse()
async sendOtpRq(@Body() body: UserLoginDto) { async sendOtpRq(@Body() body: UserLoginDto) {
@@ -40,7 +40,8 @@ export class UserAuthController {
@UseGuards(LocalUserAuthGuard) @UseGuards(LocalUserAuthGuard)
@ApiBody({ @ApiBody({
type: UserVerifyOtp, 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() @ApiAcceptedResponse()
async login(@Body() body, @Req() req, @CurrentUser() user) { async login(@Body() body, @Req() req, @CurrentUser() user) {

View File

@@ -143,6 +143,7 @@ import {
} from "src/helpers/claim-capture-phase"; } from "src/helpers/claim-capture-phase";
import { HttpService } from "@nestjs/axios"; import { HttpService } from "@nestjs/axios";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
@Injectable() @Injectable()
export class ClaimRequestManagementService { export class ClaimRequestManagementService {
@@ -4660,7 +4661,7 @@ export class ClaimRequestManagementService {
"money.sheba": shebaNumber, "money.sheba": shebaNumber,
"money.nationalCodeOfInsurer": nationalCode, "money.nationalCodeOfInsurer": nationalCode,
status: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, status: ClaimCaseStatus.CAPTURING_PART_DAMAGES,
"workflow.currentStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES, "workflow.currentStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
"workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS, "workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
@@ -6074,6 +6075,9 @@ export class ClaimRequestManagementService {
"workflow.nextStep": ClaimWorkflowStep.EXPERT_COST_EVALUATION, "workflow.nextStep": ClaimWorkflowStep.EXPERT_COST_EVALUATION,
"evaluation.objection": objectionPayload, "evaluation.objection": objectionPayload,
"damage.selectedParts": mergedNorm, "damage.selectedParts": mergedNorm,
...(pendingResendGate && {
"evaluation.damageExpertResend.fulfilledAt": new Date(),
}),
}, },
$unset: { $unset: {
"evaluation.ownerPricedPartsApproval": "", "evaluation.ownerPricedPartsApproval": "",
@@ -6657,24 +6661,15 @@ export class ClaimRequestManagementService {
} }
} }
const damagedParts = displayParts.map((sp, index) => { const damagedParts = buildEnrichedDamagedParts({
const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp); selectedParts: displayParts,
const cap = getDamagedPartCaptureBlob( damagedPartsData: damagedPartsData,
damagedPartsData, evaluationBlock: (claim as any).evaluation,
ck, expertAddedParts: (claim.damage as any)?.expertAddedParts ?? [],
selectedNormDetails, getDamagedPartCaptureBlob,
) as { url?: string; path?: string; fileName?: string } | undefined; hasDamagedPartCapture,
return { catalogLikeKeyFromPart,
index, buildFileLink,
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 er = claim.evaluation?.damageExpertResend; const er = claim.evaluation?.damageExpertResend;

View File

@@ -32,13 +32,17 @@ export function resendRequestHasPayload(r: {
}): boolean { }): boolean {
if (!r) return false; if (!r) return false;
if (String(r.resendDescription || "").trim() !== "") return true; if (String(r.resendDescription || "").trim() !== "") return true;
if (Array.isArray(r.resendDocuments) && r.resendDocuments.length > 0) return true; if (Array.isArray(r.resendDocuments) && r.resendDocuments.length > 0)
if (Array.isArray(r.resendCarParts) && r.resendCarParts.length > 0) return true; return true;
if (Array.isArray(r.resendCarParts) && r.resendCarParts.length > 0)
return true;
return false; return false;
} }
/** Normalize expert-requested document keys (strings or `{ key }`) to canonical enum strings. */ /** 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 []; if (!Array.isArray(docs)) return [];
const keys: string[] = []; const keys: string[] = [];
for (const d of docs) { for (const d of docs) {
@@ -61,10 +65,7 @@ export function normalizeResendPartKeys(
selectedParts?: unknown, selectedParts?: unknown,
): string[] { ): string[] {
if (!Array.isArray(parts)) return []; if (!Array.isArray(parts)) return [];
const selectedNorm = normalizeDamageSelectedParts( const selectedNorm = normalizeDamageSelectedParts(selectedParts, carType);
selectedParts,
carType,
);
const keys: string[] = []; const keys: string[] = [];
for (const p of parts) { for (const p of parts) {
const enriched = const enriched =
@@ -72,8 +73,7 @@ export function normalizeResendPartKeys(
matchResendPartFromSelected(p, selectedNorm); matchResendPartFromSelected(p, selectedNorm);
if (enriched) { if (enriched) {
const ck = const ck =
enriched.catalogKey?.trim() || enriched.catalogKey?.trim() || catalogLikeKeyFromPart(enriched);
catalogLikeKeyFromPart(enriched);
if (ck) keys.push(ck); if (ck) keys.push(ck);
continue; continue;
} }
@@ -108,7 +108,10 @@ function resolveResendPartRow(
const fromSelected = matchResendPartFromSelected(raw, selectedNorm ?? []); const fromSelected = matchResendPartFromSelected(raw, selectedNorm ?? []);
if (fromSelected) return fromSelected; if (fromSelected) return fromSelected;
if (fromRaw) return fromRaw; if (fromRaw) return fromRaw;
const o = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>; const o = (raw && typeof raw === "object" ? raw : {}) as Record<
string,
unknown
>;
const fallbackKey = const fallbackKey =
typeof o.key === "string" && o.key.trim() typeof o.key === "string" && o.key.trim()
? o.key.trim() ? o.key.trim()
@@ -123,8 +126,7 @@ function resolveResendPartRow(
typeof o.label_fa === "string" && o.label_fa.trim() typeof o.label_fa === "string" && o.label_fa.trim()
? o.label_fa.trim() ? o.label_fa.trim()
: fallbackKey, : fallbackKey,
catalogKey: catalogKey: typeof o.catalogKey === "string" ? o.catalogKey : undefined,
typeof o.catalogKey === "string" ? o.catalogKey : undefined,
}; };
} }
@@ -149,8 +151,7 @@ export function mapExpertResendCarPartsForClient(
const stored = const stored =
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {}; raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
const sp = resolveResendPartRow(raw, options.carType, selectedNorm); const sp = resolveResendPartRow(raw, options.carType, selectedNorm);
const catalogKey = const catalogKey = sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp);
sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp);
const captureKey = catalogKey || partLookupKey(sp); const captureKey = catalogKey || partLookupKey(sp);
const cap = getDamagedPartCaptureBlob( const cap = getDamagedPartCaptureBlob(
options.damagedPartsData, options.damagedPartsData,
@@ -193,8 +194,7 @@ export function normalizeResendCarPartsForStorage(
const stored = const stored =
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {}; raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
const sp = resolveResendPartRow(raw, carType, selectedNorm); const sp = resolveResendPartRow(raw, carType, selectedNorm);
const catalogKey = const catalogKey = sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp);
sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp);
const row: Record<string, unknown> = { const row: Record<string, unknown> = {
id: sp.id, id: sp.id,
name: sp.name, name: sp.name,
@@ -217,8 +217,13 @@ function getRequiredDocEntry(claim: any, documentKey: string): unknown {
return rd instanceof Map ? rd.get(documentKey) : rd[documentKey]; return rd instanceof Map ? rd.get(documentKey) : rd[documentKey];
} }
export function isRequiredDocumentUploaded(claim: any, documentKey: string): boolean { export function isRequiredDocumentUploaded(
const doc = getRequiredDocEntry(claim, documentKey) as { uploaded?: boolean } | undefined; claim: any,
documentKey: string,
): boolean {
const doc = getRequiredDocEntry(claim, documentKey) as
| { uploaded?: boolean }
| undefined;
return !!doc?.uploaded; return !!doc?.uploaded;
} }
@@ -241,7 +246,8 @@ export function hasDamagedPartCapture(claim: any, partKey: string): boolean {
export function canFinalizeExpertResend(claim: any): boolean { export function canFinalizeExpertResend(claim: any): boolean {
if (!claim) return false; if (!claim) return false;
if (claim.status !== ClaimCaseStatus.WAITING_FOR_USER_RESEND) 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; const r = claim.evaluation?.damageExpertResend;
if (!r || r.fulfilledAt) return false; if (!r || r.fulfilledAt) return false;
@@ -279,16 +285,39 @@ export function documentKeyAllowedForExpertResend(
return allowed.has(normalized); 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; const r = claim?.evaluation?.damageExpertResend;
if (!r || r.fulfilledAt) return false; if (!r || r.fulfilledAt) return false;
const carType = claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined; const carType = claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
const allowed = new Set( const selectedParts = claim?.damage?.selectedParts;
normalizeResendPartKeys(
r.resendCarParts, // Normalize the stored resend parts to their canonical catalog keys
carType, const allowedKeys = new Set(
claim?.damage?.selectedParts, 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));
} }

View File

@@ -8,20 +8,27 @@ const REQUIRED_VALUES = new Set<string>(
* Non-canonical document keys that may still appear on stored * Non-canonical document keys that may still appear on stored
* `evaluation.damageExpertResend.resendDocuments` → {@link ClaimRequiredDocumentType}. * `evaluation.damageExpertResend.resendDocuments` → {@link ClaimRequiredDocumentType}.
*/ */
const RESEND_DOCUMENT_KEY_ALIASES: Readonly< const RESEND_DOCUMENT_KEY_ALIASES = {
Record<string, ClaimRequiredDocumentType> // existing entries
> = {
nationalCertificate: ClaimRequiredDocumentType.NATIONAL_CARD, nationalCertificate: ClaimRequiredDocumentType.NATIONAL_CARD,
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,
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,
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,
CarCertificate: ClaimRequiredDocumentType.DAMAGED_CAR_CARD_FRONT, CarCertificate: ClaimRequiredDocumentType.DAMAGED_CAR_CARD_FRONT,
car_certificate: ClaimRequiredDocumentType.CAR_CERTIFICATE, // ← add
plate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE, plate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE,
carPlate: ClaimRequiredDocumentType.DAMAGED_METAL_PLATE, carPlate: 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. */ /** Normalize a single key to a {@link ClaimRequiredDocumentType} value, or null if unknown. */

View File

@@ -62,7 +62,9 @@ export function findCatalogItemByPersianLabel(
/** Non-catalog lines (expert-added interior / free-text); never use catalog numeric ids. */ /** Non-catalog lines (expert-added interior / free-text); never use catalog numeric ids. */
export function isInternalDamageSide(side: string): boolean { 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; if (!s || s === "internal" || s === "other") return true;
return !SIDE_SET.has(s); return !SIDE_SET.has(s);
} }
@@ -120,7 +122,9 @@ const SIDE_LABEL_FA: Record<string, string> = {
[OuterPartSideV2.TOP]: "بالا", [OuterPartSideV2.TOP]: "بالا",
}; };
function outerCatalogListForItem(item: OuterPartCatalogItem): OuterPartCatalogItem[] { function outerCatalogListForItem(
item: OuterPartCatalogItem,
): OuterPartCatalogItem[] {
for (const list of Object.values(OUTER_PARTS_BY_CAR_TYPE)) { for (const list of Object.values(OUTER_PARTS_BY_CAR_TYPE)) {
if (list.some((x) => x.key === item.key || x.id === item.id)) { if (list.some((x) => x.key === item.key || x.id === item.id)) {
return list; 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. */ /** Strip leading `left_|right_|…` when present so `name` is side-agnostic for clients. */
export function splitCatalogKeyToNameAndSide( export function splitCatalogKeyToNameAndSide(fullKey: string): {
fullKey: string, name: string;
): { name: string; side: string } { side: string;
} {
const t = String(fullKey ?? "").trim(); const t = String(fullKey ?? "").trim();
const i = t.indexOf("_"); const i = t.indexOf("_");
if (i <= 0) return { name: t, side: "" }; if (i <= 0) return { name: t, side: "" };
@@ -329,7 +334,8 @@ export function normalizeDamageSelectedParts(
let catItem: OuterPartCatalogItem | undefined; let catItem: OuterPartCatalogItem | undefined;
if (catalog && byCatalogKey) { if (catalog && byCatalogKey) {
if (ck) catItem = byCatalogKey.get(ck); 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) { if (!catItem && side && name) {
catItem = byCatalogKey.get(catalogLikeKeyFromPart({ side, name })); catItem = byCatalogKey.get(catalogLikeKeyFromPart({ side, name }));
} }
@@ -377,7 +383,9 @@ export function normalizeDamageSelectedParts(
const cat = const cat =
byCatalogKey?.get(k) ?? byCatalogKey?.get(k) ??
findCatalogItemByKey(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) { if (cat) {
const list = catalog ?? outerCatalogListForItem(cat); const list = catalog ?? outerCatalogListForItem(cat);
out.push(catalogItemToSelectedPart(cat, list)); out.push(catalogItemToSelectedPart(cat, list));
@@ -581,7 +589,9 @@ export function catalogPartIdFromCarPartDamage(
export const partIdentityKeyFromCarPartDamage = catalogPartIdFromCarPartDamage; export const partIdentityKeyFromCarPartDamage = catalogPartIdFromCarPartDamage;
function normPartSegment(s: string): string { 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( function looseCatalogNameMatch(
@@ -601,9 +611,13 @@ export function findCatalogItemFromLegacyExpertPart(
side: string, side: string,
carType?: ClaimVehicleTypeV2, carType?: ClaimVehicleTypeV2,
): OuterPartCatalogItem | undefined { ): OuterPartCatalogItem | undefined {
const s = String(side ?? "").toLowerCase().trim(); const s = String(side ?? "")
.toLowerCase()
.trim();
const lists = carType 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[][]); : (Object.values(OUTER_PARTS_BY_CAR_TYPE) as OuterPartCatalogItem[][]);
for (const catalog of lists) { for (const catalog of lists) {
if (!catalog?.length) continue; if (!catalog?.length) continue;
@@ -615,7 +629,9 @@ export function findCatalogItemFromLegacyExpertPart(
return undefined; return undefined;
} }
function selectedPartToStoredRecord(p: DamageSelectedPartV2): Record<string, unknown> { function selectedPartToStoredRecord(
p: DamageSelectedPartV2,
): Record<string, unknown> {
const r: Record<string, unknown> = { const r: Record<string, unknown> = {
name: p.name, name: p.name,
side: String(p.side || "").toLowerCase(), side: String(p.side || "").toLowerCase(),
@@ -780,9 +796,7 @@ export function isDamagedPartsMediaArray(data: unknown): boolean {
return Array.isArray(data); return Array.isArray(data);
} }
function getLegacyMapRecord( function getLegacyMapRecord(data: unknown): Record<string, unknown> | null {
data: unknown,
): Record<string, unknown> | null {
if (data == null) return null; if (data == null) return null;
if (data instanceof Map) { if (data instanceof Map) {
return Object.fromEntries((data as Map<string, unknown>).entries()); return Object.fromEntries((data as Map<string, unknown>).entries());
@@ -819,11 +833,9 @@ export function migrateLegacyDamagedPartsMapToArray(
selected: DamageSelectedPartV2[], selected: DamageSelectedPartV2[],
): Record<string, unknown>[] { ): Record<string, unknown>[] {
return selected.map((sp) => { return selected.map((sp) => {
const keysTry = [ const keysTry = [sp.catalogKey, catalogLikeKeyFromPart(sp), sp.name].filter(
sp.catalogKey, Boolean,
catalogLikeKeyFromPart(sp), ) as string[];
sp.name,
].filter(Boolean) as string[];
let blob: unknown; let blob: unknown;
for (const k of keysTry) { for (const k of keysTry) {
blob = getCaptureBlobLoose(legacyMap, k); blob = getCaptureBlobLoose(legacyMap, k);
@@ -887,12 +899,14 @@ export function resolvePartCaptureIndex(
if (!raw) return -1; if (!raw) return -1;
if (selected.length) { if (selected.length) {
// Pass 1: exact matches on catalogKey and catalogLikeKey
for (let i = 0; i < selected.length; i++) { for (let i = 0; i < selected.length; i++) {
const sp = selected[i]; const sp = selected[i];
if (sp.catalogKey === raw) return i; if (sp.catalogKey === raw) return i;
if (catalogLikeKeyFromPart(sp) === raw) return i; if (catalogLikeKeyFromPart(sp) === raw) return i;
} }
// Pass 2: numeric id or array index
if (/^\d+$/.test(raw)) { if (/^\d+$/.test(raw)) {
const n = Number(raw); const n = Number(raw);
const byId = selected.findIndex((p) => p.id === n); const byId = selected.findIndex((p) => p.id === n);
@@ -901,31 +915,35 @@ export function resolvePartCaptureIndex(
if (!hasIdCollision && n >= 0 && n < selected.length) return n; 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, ""); const loose = raw.toLowerCase().replace(/[\s_-]/g, "");
for (let i = 0; i < selected.length; i++) { for (let i = 0; i < selected.length; i++) {
const sp = selected[i]; const sp = selected[i];
const cand = [ const candidates = [
sp.catalogKey, sp.catalogKey,
catalogLikeKeyFromPart(sp), catalogLikeKeyFromPart(sp),
sp.name, sp.name,
].filter(Boolean) as string[]; ].filter(Boolean) as string[];
for (const c of cand) { for (const c of candidates) {
if (c.toLowerCase().replace(/[\s_-]/g, "") === loose) return i; if (c.toLowerCase().replace(/[\s_-]/g, "") === loose) return i;
} }
} }
}
if (damagedPartsArray && damagedPartsArray.length) { // Pass 4: suffix match — handles car-type-prefixed catalog keys
for (let i = 0; i < damagedPartsArray.length; i++) { // e.g. "sedan_front_hood" should match sp.catalogKey="front_hood"
const row = damagedPartsArray[i]; // or normalizeResendPartKeys output "sedan_front_hood" vs stored "front_hood"
if (!row || typeof row !== "object") continue; for (let i = 0; i < selected.length; i++) {
const id = toNum((row as { id?: unknown }).id); const sp = selected[i];
if (id != null && String(id) === raw) return i; const candidates = [
const ck = String((row as { catalogKey?: unknown }).catalogKey || ""); sp.catalogKey,
if (ck && ck === raw) return i; catalogLikeKeyFromPart(sp),
const name = String((row as { name?: unknown }).name || ""); sp.name,
const side = String((row as { side?: unknown }).side || ""); ].filter(Boolean) as string[];
if (side && name && `${side}_${name}` === raw) return i; 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 { ): boolean {
if (damagedPartsData == null) return false; if (damagedPartsData == null) return false;
if (Array.isArray(damagedPartsData)) { 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; if (idx < 0) return false;
return mediaRowHasCapture((damagedPartsData as unknown[])[idx]); return mediaRowHasCapture((damagedPartsData as unknown[])[idx]);
} }

View File

@@ -41,7 +41,10 @@ export class SmsOrchestrationService implements OnModuleInit {
return `${process.env.URL}/${frontendRoute}?token=${requestId}`; 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"; const route = partyRole === "SECOND" ? "user2" : "user";
return `${process.env.URL}/${route}?token=${requestId}`; return `${process.env.URL}/${route}?token=${requestId}`;
} }
@@ -50,7 +53,11 @@ export class SmsOrchestrationService implements OnModuleInit {
return `${process.env.URL}/caseClaim?token=${claimRequestId}`; return `${process.env.URL}/caseClaim?token=${claimRequestId}`;
} }
async sendInviteLink(phoneNumber: string, publicId: string, link: string): Promise<boolean> { async sendInviteLink(
phoneNumber: string,
publicId: string,
link: string,
): Promise<boolean> {
return this.sendTemplate({ return this.sendTemplate({
template: "yara724-invite-link", template: "yara724-invite-link",
receptor: phoneNumber, receptor: phoneNumber,
@@ -132,14 +139,12 @@ export class SmsOrchestrationService implements OnModuleInit {
}); });
} }
async sendThirdPartyExpertStartedReviewNotice( async sendThirdPartyExpertStartedReviewNotice(params: {
params: {
receptor: string; receptor: string;
fileKind: "blame" | "claim"; fileKind: "blame" | "claim";
publicId: string; publicId: string;
expertLastName: string; expertLastName: string;
}, }): Promise<boolean> {
): Promise<boolean> {
return this.sendTemplate({ return this.sendTemplate({
template: "yara-expert-lock", template: "yara-expert-lock",
receptor: params.receptor, receptor: params.receptor,