forked from Yara724/api
Fixed resendCarParts label_fa's + OTP
This commit is contained in:
@@ -1,10 +1,30 @@
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||
import { getDamagedPartCaptureBlob } from "./claim-car-angle-media";
|
||||
import { normalizeDamageSelectedParts } from "./outer-damage-parts";
|
||||
import {
|
||||
catalogLikeKeyFromPart,
|
||||
DamageSelectedPartV2,
|
||||
normalizeDamageSelectedParts,
|
||||
partLookupKey,
|
||||
} from "./outer-damage-parts";
|
||||
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||
import { canonicalizeResendDocumentKey } from "./claim-resend-document-keys";
|
||||
|
||||
export type ExpertResendCarPartV2 = {
|
||||
id?: number | null;
|
||||
name: string;
|
||||
side: string;
|
||||
label_fa: string;
|
||||
label_en?: string;
|
||||
catalogKey?: string;
|
||||
/** Same as `catalogKey` — kept for clients that use `key` on capture/resend APIs. */
|
||||
key?: string;
|
||||
captured: boolean;
|
||||
url?: string;
|
||||
path?: string;
|
||||
fileName?: string;
|
||||
};
|
||||
|
||||
export function resendRequestHasPayload(r: {
|
||||
resendDescription?: string;
|
||||
resendDocuments?: unknown[];
|
||||
@@ -34,18 +54,161 @@ export function normalizeResendDocumentKeys(docs: unknown[] | undefined): string
|
||||
return [...new Set(keys)];
|
||||
}
|
||||
|
||||
/** Normalize expert-requested damaged part keys (`DamagedPartItem.key`). */
|
||||
export function normalizeResendPartKeys(parts: unknown[] | undefined): string[] {
|
||||
/** Capture / finalize lookup keys for expert-requested damaged parts. */
|
||||
export function normalizeResendPartKeys(
|
||||
parts: unknown[] | undefined,
|
||||
carType?: ClaimVehicleTypeV2,
|
||||
selectedParts?: unknown,
|
||||
): string[] {
|
||||
if (!Array.isArray(parts)) return [];
|
||||
const selectedNorm = normalizeDamageSelectedParts(
|
||||
selectedParts,
|
||||
carType,
|
||||
);
|
||||
const keys: string[] = [];
|
||||
for (const p of parts) {
|
||||
const enriched =
|
||||
normalizeDamageSelectedParts([p], carType)[0] ||
|
||||
matchResendPartFromSelected(p, selectedNorm);
|
||||
if (enriched) {
|
||||
const ck =
|
||||
enriched.catalogKey?.trim() ||
|
||||
catalogLikeKeyFromPart(enriched);
|
||||
if (ck) keys.push(ck);
|
||||
continue;
|
||||
}
|
||||
if (typeof p === "string" && p.trim()) keys.push(p.trim());
|
||||
else if (p && typeof p === "object" && "key" in (p as object)) {
|
||||
const k = String((p as { key?: string }).key || "").trim();
|
||||
if (k) keys.push(k);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
return [...new Set(keys)];
|
||||
}
|
||||
|
||||
function matchResendPartFromSelected(
|
||||
raw: unknown,
|
||||
selectedNorm: DamageSelectedPartV2[],
|
||||
): DamageSelectedPartV2 | undefined {
|
||||
if (!raw || typeof raw !== "object" || !selectedNorm.length) return undefined;
|
||||
const o = raw as { id?: number };
|
||||
if (typeof o.id === "number" && Number.isFinite(o.id)) {
|
||||
return selectedNorm.find((sp) => sp.id === Math.trunc(o.id));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveResendPartRow(
|
||||
raw: unknown,
|
||||
carType?: ClaimVehicleTypeV2,
|
||||
selectedNorm?: DamageSelectedPartV2[],
|
||||
): DamageSelectedPartV2 {
|
||||
const fromRaw = normalizeDamageSelectedParts([raw], carType)[0];
|
||||
if (fromRaw?.label_fa && fromRaw.name) return fromRaw;
|
||||
const fromSelected = matchResendPartFromSelected(raw, selectedNorm ?? []);
|
||||
if (fromSelected) return fromSelected;
|
||||
if (fromRaw) return fromRaw;
|
||||
const o = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
|
||||
const fallbackKey =
|
||||
typeof o.key === "string" && o.key.trim()
|
||||
? o.key.trim()
|
||||
: typeof raw === "string"
|
||||
? String(raw).trim()
|
||||
: "unknown";
|
||||
return {
|
||||
id: typeof o.id === "number" ? Math.trunc(o.id) : null,
|
||||
name: typeof o.name === "string" ? o.name : fallbackKey,
|
||||
side: typeof o.side === "string" ? o.side : "",
|
||||
label_fa:
|
||||
typeof o.label_fa === "string" && o.label_fa.trim()
|
||||
? o.label_fa.trim()
|
||||
: fallbackKey,
|
||||
catalogKey:
|
||||
typeof o.catalogKey === "string" ? o.catalogKey : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** Enrich stored resend rows for claim detail / capture UIs (label_fa, catalogKey, capture state). */
|
||||
export function mapExpertResendCarPartsForClient(
|
||||
rawParts: unknown[] | undefined,
|
||||
options: {
|
||||
carType?: ClaimVehicleTypeV2;
|
||||
selectedParts?: unknown;
|
||||
damagedPartsData?: unknown;
|
||||
buildUrl?: (path: string) => string;
|
||||
} = {},
|
||||
): ExpertResendCarPartV2[] {
|
||||
if (!Array.isArray(rawParts) || rawParts.length === 0) return [];
|
||||
|
||||
const selectedNorm = normalizeDamageSelectedParts(
|
||||
options.selectedParts,
|
||||
options.carType,
|
||||
);
|
||||
|
||||
return rawParts.map((raw) => {
|
||||
const stored =
|
||||
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
|
||||
const sp = resolveResendPartRow(raw, options.carType, selectedNorm);
|
||||
const catalogKey =
|
||||
sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp);
|
||||
const captureKey = catalogKey || partLookupKey(sp);
|
||||
const cap = getDamagedPartCaptureBlob(
|
||||
options.damagedPartsData,
|
||||
captureKey,
|
||||
selectedNorm,
|
||||
) as { url?: string; path?: string; fileName?: string } | undefined;
|
||||
const capturedFromStore =
|
||||
typeof stored.captured === "boolean" ? stored.captured : undefined;
|
||||
const url =
|
||||
cap?.url ||
|
||||
(cap?.path && options.buildUrl ? options.buildUrl(cap.path) : undefined);
|
||||
|
||||
return {
|
||||
id: sp.id,
|
||||
name: sp.name,
|
||||
side: sp.side,
|
||||
label_fa: sp.label_fa,
|
||||
...(typeof stored.label_en === "string" && stored.label_en.trim()
|
||||
? { label_en: stored.label_en.trim() }
|
||||
: {}),
|
||||
catalogKey,
|
||||
key: catalogKey,
|
||||
captured: capturedFromStore ?? !!cap,
|
||||
...(url ? { url } : {}),
|
||||
...(cap?.path ? { path: cap.path } : {}),
|
||||
...(cap?.fileName ? { fileName: cap.fileName } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Persistable snapshot for `evaluation.damageExpertResend.resendCarParts`. */
|
||||
export function normalizeResendCarPartsForStorage(
|
||||
rawParts: unknown[] | undefined,
|
||||
carType?: ClaimVehicleTypeV2,
|
||||
selectedParts?: unknown,
|
||||
): Array<Record<string, unknown>> {
|
||||
if (!Array.isArray(rawParts)) return [];
|
||||
const selectedNorm = normalizeDamageSelectedParts(selectedParts, carType);
|
||||
return rawParts.map((raw) => {
|
||||
const stored =
|
||||
raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
|
||||
const sp = resolveResendPartRow(raw, carType, selectedNorm);
|
||||
const catalogKey =
|
||||
sp.catalogKey?.trim() || catalogLikeKeyFromPart(sp);
|
||||
const row: Record<string, unknown> = {
|
||||
id: sp.id,
|
||||
name: sp.name,
|
||||
side: sp.side,
|
||||
label_fa: sp.label_fa,
|
||||
catalogKey,
|
||||
key: catalogKey,
|
||||
captured: false,
|
||||
};
|
||||
if (typeof stored.label_en === "string" && stored.label_en.trim()) {
|
||||
row.label_en = stored.label_en.trim();
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
function getRequiredDocEntry(claim: any, documentKey: string): unknown {
|
||||
@@ -62,9 +225,10 @@ export function isRequiredDocumentUploaded(claim: any, documentKey: string): boo
|
||||
export function hasDamagedPartCapture(claim: any, partKey: string): boolean {
|
||||
const data = claim?.media?.damagedParts;
|
||||
if (!data) return false;
|
||||
const carType = claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||
const norm = normalizeDamageSelectedParts(
|
||||
claim?.damage?.selectedParts,
|
||||
claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined,
|
||||
carType,
|
||||
claim?.damage?.selectedOuterParts,
|
||||
);
|
||||
return getDamagedPartCaptureBlob(data, partKey, norm) != null;
|
||||
@@ -82,8 +246,13 @@ export function canFinalizeExpertResend(claim: any): boolean {
|
||||
const r = claim.evaluation?.damageExpertResend;
|
||||
if (!r || r.fulfilledAt) return false;
|
||||
|
||||
const carType = claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||
const docKeys = normalizeResendDocumentKeys(r.resendDocuments);
|
||||
const partKeys = normalizeResendPartKeys(r.resendCarParts);
|
||||
const partKeys = normalizeResendPartKeys(
|
||||
r.resendCarParts,
|
||||
carType,
|
||||
claim?.damage?.selectedParts,
|
||||
);
|
||||
|
||||
if (docKeys.length === 0 && partKeys.length === 0) {
|
||||
return String(r.resendDescription || "").trim() !== "";
|
||||
@@ -113,6 +282,13 @@ export function documentKeyAllowedForExpertResend(
|
||||
export function partKeyAllowedForExpertResend(claim: any, partKey: string): boolean {
|
||||
const r = claim?.evaluation?.damageExpertResend;
|
||||
if (!r || r.fulfilledAt) return false;
|
||||
const allowed = new Set(normalizeResendPartKeys(r.resendCarParts));
|
||||
const carType = claim?.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||
const allowed = new Set(
|
||||
normalizeResendPartKeys(
|
||||
r.resendCarParts,
|
||||
carType,
|
||||
claim?.damage?.selectedParts,
|
||||
),
|
||||
);
|
||||
return allowed.has(partKey);
|
||||
}
|
||||
|
||||
46
src/helpers/iran-mobile.ts
Normal file
46
src/helpers/iran-mobile.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { FilterQuery } from "mongoose";
|
||||
import { UserModel } from "src/users/entities/schema/user.schema";
|
||||
|
||||
/** Canonical Iranian mobile: leading 0 + 10 digits (e.g. 09123456789). */
|
||||
export function normalizeIranMobile(phone?: string): string | undefined {
|
||||
if (!phone) return undefined;
|
||||
|
||||
const digits = String(phone).replace(/\D/g, "");
|
||||
if (!digits) return undefined;
|
||||
if (digits.startsWith("0098")) return `0${digits.slice(4)}`;
|
||||
if (digits.startsWith("98") && digits.length === 12) {
|
||||
return `0${digits.slice(2)}`;
|
||||
}
|
||||
if (digits.length === 10 && digits.startsWith("9")) return `0${digits}`;
|
||||
if (digits.length === 11 && digits.startsWith("0")) return digits;
|
||||
return digits;
|
||||
}
|
||||
|
||||
/** Variants that may exist on legacy user rows (username/mobile). */
|
||||
export function iranMobileLookupVariants(phone?: string): string[] {
|
||||
const raw = phone?.trim();
|
||||
const canonical = normalizeIranMobile(raw);
|
||||
const variants = new Set<string>();
|
||||
if (raw) variants.add(raw);
|
||||
if (canonical) {
|
||||
variants.add(canonical);
|
||||
if (canonical.startsWith("0") && canonical.length === 11) {
|
||||
variants.add(canonical.slice(1));
|
||||
variants.add(`98${canonical.slice(1)}`);
|
||||
variants.add(`0098${canonical.slice(1)}`);
|
||||
}
|
||||
}
|
||||
return Array.from(variants).filter(Boolean);
|
||||
}
|
||||
|
||||
export function buildUserLookupByPhone(
|
||||
phone: string,
|
||||
): FilterQuery<UserModel> {
|
||||
const variants = iranMobileLookupVariants(phone);
|
||||
if (variants.length === 0) {
|
||||
return { username: phone };
|
||||
}
|
||||
return {
|
||||
$or: variants.flatMap((v) => [{ username: v }, { mobile: v }]),
|
||||
};
|
||||
}
|
||||
@@ -301,6 +301,19 @@ export function normalizeDamageSelectedParts(
|
||||
}
|
||||
if (typeof el === "object") {
|
||||
const o = el as Record<string, unknown>;
|
||||
const idOnly = toNum(o.id);
|
||||
const hasName = typeof o.name === "string" && o.name.trim();
|
||||
const hasKey = typeof o.key === "string" && String(o.key).trim();
|
||||
if (idOnly != null && !hasName && !hasKey) {
|
||||
let catItem: OuterPartCatalogItem | undefined;
|
||||
if (catalog) catItem = catalog.find((c) => c.id === idOnly);
|
||||
if (!catItem) catItem = CATALOG_ITEM_BY_ID.get(idOnly);
|
||||
if (catItem) {
|
||||
const list = catalog ?? outerCatalogListForItem(catItem);
|
||||
out.push(catalogItemToSelectedPart(catItem, list));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (typeof o.name === "string" && o.name.trim()) {
|
||||
let name = o.name.trim();
|
||||
const inner = splitCatalogKeyToNameAndSide(name);
|
||||
|
||||
43
src/helpers/user-otp-expiry.ts
Normal file
43
src/helpers/user-otp-expiry.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/** OTP expiry is stored as epoch milliseconds on user documents. */
|
||||
export function otpExpireToEpochMs(value: unknown): number {
|
||||
if (value == null) return 0;
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
if (value >= 1_000_000_000_000) return value;
|
||||
if (value >= 1_000_000_000 && value < 1_000_000_000_000) {
|
||||
return value * 1000;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value.getTime();
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const asNum = Number(value);
|
||||
if (Number.isFinite(asNum)) return asNum;
|
||||
const parsed = Date.parse(value);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function isOtpExpiryActive(
|
||||
otpExpire: unknown,
|
||||
nowMs: number = Date.now(),
|
||||
): boolean {
|
||||
const exp = otpExpireToEpochMs(otpExpire);
|
||||
return exp > nowMs;
|
||||
}
|
||||
|
||||
export function computeOtpExpireMs(
|
||||
expireMinutes: number,
|
||||
nowMs: number = Date.now(),
|
||||
): number {
|
||||
const minutes =
|
||||
Number.isFinite(expireMinutes) && expireMinutes > 0 ? expireMinutes : 2;
|
||||
return nowMs + minutes * 60 * 1000;
|
||||
}
|
||||
|
||||
export function readOtpExpireMinutesFromEnv(): number {
|
||||
const raw = Number(process.env.EXP_OTP_TIME ?? "2");
|
||||
return Number.isFinite(raw) && raw > 0 ? raw : 2;
|
||||
}
|
||||
Reference in New Issue
Block a user