forked from Yara724/api
Fixed damagedParts unified structure and resend problems
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user