script updated , inquiry field added to blame case and claim case

This commit is contained in:
2026-06-02 12:32:49 +03:30
parent 8730e9af62
commit fe82455562
6 changed files with 580 additions and 108 deletions

View File

@@ -3,7 +3,8 @@
/*
* One-time script to replace mocked blameCases party vehicle inquiry data.
* THIRD_PARTY cases use Tejarat block inquiry; CAR_BODY cases use both
* Tejarat third-party block inquiry and car-body inquiry.
* Tejarat third-party block inquiry and car-body inquiry. All cases also
* refresh available party personal inquiries into inquiries.person.
*
* Defaults to DRY_RUN=true. Set DRY_RUN=false to write changes.
*/
@@ -61,10 +62,15 @@ const config = {
carBodyUrl:
process.env.TEJARAT_CAR_BODY_URL ||
"http://82.99.202.245:3027/block-inquiry-tejarat/badane",
personUrl:
process.env.TEJARAT_PERSON_URL ||
"http://82.99.202.245:3027/personal-inquiry/tejarat-no",
thirdPartyToken:
process.env.TEJARAT_THIRD_PARTY_TOKEN || process.env.TEJARAT_TOKEN || "",
carBodyToken:
process.env.TEJARAT_CAR_BODY_TOKEN || process.env.TEJARAT_TOKEN || "",
personToken:
process.env.TEJARAT_PERSON_TOKEN || process.env.TEJARAT_TOKEN || "",
rateLimitPerMinute: Number(process.env.RATE_LIMIT_PER_MINUTE || 5),
retryEnabled: String(process.env.RETRY_ENABLED ?? "true").toLowerCase() !== "false",
retryCount: Number(process.env.RETRY_COUNT || 3),
@@ -124,6 +130,7 @@ async function main() {
requestNo: 1,
type: 1,
parties: 1,
inquiries: 1,
},
})
.sort({ createdAt: 1, _id: 1 });
@@ -136,6 +143,9 @@ async function main() {
partiesInquired: 0,
partiesSkipped: 0,
partiesFailed: 0,
personInquired: 0,
personSkipped: 0,
personFailed: 0,
};
for await (const doc of cursor) {
@@ -145,7 +155,9 @@ async function main() {
console.log(`currently inquiry for doc with ${label} and requestId ${requestId}`);
const parties = Array.isArray(doc.parties) ? clone(doc.parties) : [];
const inquiries = doc.inquiries && typeof doc.inquiries === "object" ? clone(doc.inquiries) : {};
let docChanged = false;
let inquiriesChanged = false;
for (let index = 0; index < parties.length; index += 1) {
const party = parties[index];
@@ -155,61 +167,101 @@ async function main() {
if (!input.ok) {
summary.partiesSkipped += 1;
console.warn(`[skip] ${partyLabel}: ${input.reason}`);
continue;
}
} else {
let nextParty = party;
let partyChanged = false;
let nextParty = party;
let partyChanged = false;
for (const request of input.requests) {
console.log(
`[request] ${partyLabel} type=${request.type} body=${JSON.stringify(request.body)}`,
);
try {
const inquiryResponse = await inquiryWithRetry(request.type, request.body);
const successful = isInquirySuccessful(request.type, inquiryResponse);
for (const request of input.requests) {
console.log(
`[response] ${partyLabel} type=${request.type} successful=${successful} body=${JSON.stringify(inquiryResponse)}`,
`[request] ${partyLabel} type=${request.type} body=${JSON.stringify(request.body)}`,
);
if (!successful) {
summary.partiesFailed += 1;
continue;
}
try {
const inquiryResponse = await inquiryWithRetry(request.type, request.body);
const successful = isInquirySuccessful(request.type, inquiryResponse);
console.log(
`[response] ${partyLabel} type=${request.type} successful=${successful} body=${JSON.stringify(inquiryResponse)}`,
);
nextParty = applyInquiryToParty(request.type, nextParty, inquiryResponse, input.plate);
summary.partiesInquired += 1;
partyChanged = true;
} catch (error) {
summary.partiesFailed += 1;
console.error(`[error] ${partyLabel} type=${request.type}: ${error.message}`);
if (!successful) {
summary.partiesFailed += 1;
continue;
}
nextParty = applyInquiryToParty(request.type, nextParty, inquiryResponse, input.plate);
summary.partiesInquired += 1;
partyChanged = true;
} catch (error) {
summary.partiesFailed += 1;
console.error(`[error] ${partyLabel} type=${request.type}: ${error.message}`);
}
}
if (partyChanged) {
parties[index] = nextParty;
docChanged = true;
}
}
if (partyChanged) {
parties[index] = nextParty;
docChanged = true;
const personInput = buildPersonInquiryInput(party);
if (!personInput.ok) {
summary.personSkipped += 1;
console.warn(`[skip] ${partyLabel} person: ${personInput.reason}`);
continue;
}
console.log(`[request] ${partyLabel} type=PERSON body=${JSON.stringify(personInput.body)}`);
try {
const personResponse = await inquiryWithRetry("PERSON", personInput.body);
const successful = isInquirySuccessful("PERSON", personResponse);
console.log(
`[response] ${partyLabel} type=PERSON successful=${successful} body=${JSON.stringify(personResponse)}`,
);
if (!successful) {
summary.personFailed += 1;
applyPersonInquiryToCaseInquiries(inquiries, party, index, false, {}, personResponse);
inquiriesChanged = true;
continue;
}
applyPersonInquiryToCaseInquiries(
inquiries,
party,
index,
true,
normalizePersonResponse(personResponse),
);
summary.personInquired += 1;
inquiriesChanged = true;
} catch (error) {
summary.personFailed += 1;
applyPersonInquiryToCaseInquiries(inquiries, party, index, false, {}, error);
inquiriesChanged = true;
console.error(`[error] ${partyLabel} type=PERSON: ${error.message}`);
}
}
if (!docChanged) {
if (!docChanged && !inquiriesChanged) {
console.log(`[doc] ${label} no changes`);
continue;
}
if (config.dryRun) {
console.log(`[dry-run] ${label} would update parties array`);
console.log(`[dry-run] ${label} would update parties/inquiries`);
continue;
}
const updateSet = {
updatedAt: new Date(),
};
if (docChanged) updateSet.parties = parties;
if (inquiriesChanged) updateSet.inquiries = inquiries;
const result = await collection.updateOne(
{ _id: doc._id },
{
$set: {
parties,
updatedAt: new Date(),
},
$set: updateSet,
},
);
summary.docsChanged += result.modifiedCount;
@@ -285,6 +337,40 @@ function buildCarBodyRequest(plate, serialLetter, nationalCode) {
};
}
function buildPersonInquiryInput(party) {
if (!party || typeof party !== "object") return { ok: false, reason: "party is empty" };
const person = party.person && typeof party.person === "object" ? party.person : null;
if (!person) return { ok: false, reason: "party.person is missing" };
const nationalCode =
cleanString(person.nationalCodeOfInsurer) ||
cleanString(person.nationalCodeOfDriver);
const birthDate = firstPresent(
person.insurerBirthday,
person.driverBirthday,
person.birthday,
);
const gregorianBirthdate = jalaliToGregorianDate(birthDate);
if (!nationalCode) {
return { ok: false, reason: "nationalCodeOfInsurer/nationalCodeOfDriver missing" };
}
if (!gregorianBirthdate) {
return {
ok: false,
reason: `invalid insurerBirthday/driverBirthday=${cleanString(birthDate)}`,
};
}
return {
ok: true,
body: {
nationalCode,
birthdate: gregorianBirthdate,
},
};
}
function parsePlateId(plateId) {
const value = cleanString(plateId);
if (!value) return null;
@@ -352,8 +438,10 @@ function extractPlate(party) {
}
async function inquiryWithRetry(type, body) {
const url = type === "CAR_BODY" ? config.carBodyUrl : config.thirdPartyUrl;
const token = type === "CAR_BODY" ? config.carBodyToken : config.thirdPartyToken;
const endpoint = getInquiryEndpoint(type);
const url = endpoint.url;
const token = endpoint.token;
const accept = endpoint.accept;
let lastError;
const maxAttempts = config.retryEnabled ? config.retryCount : 1;
@@ -362,7 +450,7 @@ async function inquiryWithRetry(type, body) {
await waitForRateLimit();
try {
console.log("[http] " + type + " attempt=" + attempt + "/" + maxAttempts);
return await postJson(url, token, body);
return await postJson(url, token, body, accept);
} catch (error) {
lastError = error;
console.error(`[retry] ${type} attempt=${attempt} failed: ${error.message}`);
@@ -375,11 +463,11 @@ async function inquiryWithRetry(type, body) {
throw lastError;
}
async function postJson(url, token, body) {
async function postJson(url, token, body, accept = "application/json") {
const response = await fetch(url, {
method: "POST",
headers: {
accept: "application/json",
accept,
authorization: `Bearer ${token}`,
"content-type": "application/json",
},
@@ -395,7 +483,10 @@ async function postJson(url, token, body) {
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${JSON.stringify(data)}`);
const error = new Error(`HTTP ${response.status}: ${JSON.stringify(data)}`);
error.status = response.status;
error.data = data;
throw error;
}
return data;
@@ -403,9 +494,20 @@ async function postJson(url, token, body) {
function isInquirySuccessful(type, response) {
if (type === "CAR_BODY") return response && response.isSuccess === true && response.data;
if (type === "PERSON") return response && response.status === 200 && response.data;
return response && response.resultStatus === true;
}
function getInquiryEndpoint(type) {
if (type === "CAR_BODY") {
return { url: config.carBodyUrl, token: config.carBodyToken, accept: "application/json" };
}
if (type === "PERSON") {
return { url: config.personUrl, token: config.personToken, accept: "*/*" };
}
return { url: config.thirdPartyUrl, token: config.thirdPartyToken, accept: "application/json" };
}
function applyInquiryToParty(type, party, response, originalPlate) {
if (type === "CAR_BODY") return applyCarBodyInquiryToParty(party, response, originalPlate);
return applyThirdPartyInquiryToParty(party, response, originalPlate);
@@ -486,6 +588,51 @@ function applyCarBodyInquiryToParty(party, response, originalPlate) {
return next;
}
function applyPersonInquiryToCaseInquiries(inquiries, party, index, has, data, error) {
if (!inquiries.person || typeof inquiries.person !== "object") {
inquiries.person = {};
}
const existingData =
inquiries.person.data && typeof inquiries.person.data === "object" && !Array.isArray(inquiries.person.data)
? inquiries.person.data
: {};
const existingError =
inquiries.person.error && typeof inquiries.person.error === "object" && !Array.isArray(inquiries.person.error)
? inquiries.person.error
: {};
const roleKey = party && party.role ? party.role : `party_${index}`;
const nextData = { ...existingData };
const nextError = { ...existingError };
if (has) {
nextData[roleKey] = data || {};
delete nextError[roleKey];
} else {
nextError[roleKey] = normalizeInquiryError(error);
}
inquiries.person = {
has: Object.keys(nextData).length > 0,
data: nextData,
...(Object.keys(nextError).length > 0 ? { error: nextError } : {}),
updatedAt: new Date(),
};
}
function normalizePersonResponse(response) {
return response && response.data ? response.data : response;
}
function normalizeInquiryError(error) {
if (!error) return undefined;
return {
message: error.message || String(error),
status: error.status,
data: error.data,
};
}
function buildPlateId(plate) {
if (!plate) return "";
const serialLetter = NUMBER_TO_LETTER[String(plate.Plk2)] || cleanString(plate.Plk2);
@@ -555,6 +702,111 @@ function normalizeCarBodyResponse(response, originalPlate) {
};
}
function jalaliToGregorianDate(input) {
if (input === null || input === undefined) return null;
const raw = normalizeDigits(typeof input === "number" ? String(input) : String(input).trim());
if (!raw) return null;
let year = 0;
let month = 0;
let day = 0;
const separated = raw.match(/^(\d{4})[\-/](\d{1,2})[\-/](\d{1,2})$/);
if (separated) {
year = parseInt(separated[1], 10);
month = parseInt(separated[2], 10);
day = parseInt(separated[3], 10);
} else {
const digits = raw.replace(/\D/g, "");
if (digits.length !== 8) return null;
year = parseInt(digits.slice(0, 4), 10);
month = parseInt(digits.slice(4, 6), 10);
day = parseInt(digits.slice(6, 8), 10);
}
if (!year || !month || !day) return null;
if (year >= 1900) {
const mm = String(month).padStart(2, "0");
const dd = String(day).padStart(2, "0");
const result = `${year}-${mm}-${dd}`;
return isNaN(new Date(result).getTime()) ? null : result;
}
return jalaliPartsToGregorian(year, month, day);
}
function jalaliPartsToGregorian(jYear, jMonth, jDay) {
const jy = jYear - 979;
const jm = jMonth - 1;
const jd = jDay - 1;
let jDayNo =
365 * jy + Math.floor(jy / 33) * 8 + Math.floor(((jy % 33) + 3) / 4);
const jalaliMonthDays = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29];
for (let i = 0; i < jm; i += 1) {
jDayNo += jalaliMonthDays[i];
}
jDayNo += jd;
let gDayNo = jDayNo + 79;
let gy = 1600 + 400 * Math.floor(gDayNo / 146097);
gDayNo %= 146097;
let leap = true;
if (gDayNo >= 36525) {
gDayNo -= 1;
gy += 100 * Math.floor(gDayNo / 36524);
gDayNo %= 36524;
if (gDayNo >= 365) gDayNo += 1;
else leap = false;
}
gy += 4 * Math.floor(gDayNo / 1461);
gDayNo %= 1461;
if (gDayNo >= 366) {
leap = false;
gDayNo -= 1;
gy += Math.floor(gDayNo / 365);
gDayNo %= 365;
}
const gregorianMonthDays = [
31,
leap ? 29 : 28,
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let gm = 0;
for (let i = 0; i < 12; i += 1) {
if (gDayNo < gregorianMonthDays[i]) {
gm = i + 1;
break;
}
gDayNo -= gregorianMonthDays[i];
}
const gd = gDayNo + 1;
const mm = String(gm).padStart(2, "0");
const dd = String(gd).padStart(2, "0");
const result = `${gy}-${mm}-${dd}`;
return isNaN(new Date(result).getTime()) ? null : result;
}
async function waitForRateLimit() {
const minDelayMs = Math.ceil(60000 / config.rateLimitPerMinute);
const elapsed = Date.now() - lastRequestAt;
@@ -580,6 +832,9 @@ function validateConfig() {
if (!config.carBodyToken) {
throw new Error("TEJARAT_CAR_BODY_TOKEN or TEJARAT_TOKEN is required");
}
if (!config.personToken) {
throw new Error("TEJARAT_PERSON_TOKEN or TEJARAT_TOKEN is required");
}
}
function loadEnvFiles(filePath) {

View File

@@ -3973,6 +3973,7 @@ export class ClaimRequestManagementService {
blameRequestNo: blameRequest.requestNo,
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
claimStatus: ClaimStatus.PENDING,
inquiries: (blameRequest as any).inquiries ?? {},
workflow: {
currentStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
nextStep: ClaimWorkflowStep.SELECT_OTHER_PARTS,
@@ -4108,6 +4109,7 @@ export class ClaimRequestManagementService {
blameRequestNo: blameRequest.requestNo,
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
claimStatus: ClaimStatus.PENDING,
inquiries: (blameRequest as any).inquiries ?? {},
workflow: {
currentStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
nextStep: ClaimWorkflowStep.SELECT_OTHER_PARTS,
@@ -4258,6 +4260,7 @@ export class ClaimRequestManagementService {
blameRequestNo: blameRequest.requestNo,
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
claimStatus: ClaimStatus.PENDING,
inquiries: (blameRequest as any).inquiries ?? {},
workflow: {
currentStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
nextStep: ClaimWorkflowStep.SELECT_OTHER_PARTS,

View File

@@ -3,7 +3,10 @@ import { HydratedDocument, Schema as MongooseSchema, Types } from "mongoose";
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
import { HistoryEvent, HistoryEventSchema } from "src/request-management/entities/schema/historyEvent.type";
import {
HistoryEvent,
HistoryEventSchema,
} from "src/request-management/entities/schema/historyEvent.type";
import {
ClaimDamageSelection,
ClaimDamageSelectionSchema,
@@ -25,8 +28,15 @@ import {
ClaimCaseSnapshot,
ClaimCaseSnapshotSchema,
} from "./claim-case.snapshot.schema";
import { ClaimWorkflow, ClaimWorkflowSchema } from "./claim-case.workflow.schema";
import {
ClaimWorkflow,
ClaimWorkflowSchema,
} from "./claim-case.workflow.schema";
import { UserClaimRating } from "./claim-request-management.schema";
import {
CaseInquiries,
CaseInquiriesSchema,
} from "src/common/schema/case-inquiries.schema";
@Schema({ _id: false })
export class RequiredDocumentRef {
@@ -45,7 +55,8 @@ export class RequiredDocumentRef {
@Prop({ type: Date })
uploadedAt?: Date;
}
export const RequiredDocumentRefSchema = SchemaFactory.createForClass(RequiredDocumentRef);
export const RequiredDocumentRefSchema =
SchemaFactory.createForClass(RequiredDocumentRef);
@Schema({
collection: "claimCases",
@@ -67,7 +78,12 @@ export class ClaimCase {
/**
* Overall case status (user flow + expert flow progression)
*/
@Prop({ required: true, type: String, enum: ClaimCaseStatus, default: ClaimCaseStatus.CREATED })
@Prop({
required: true,
type: String,
enum: ClaimCaseStatus,
default: ClaimCaseStatus.CREATED,
})
status: ClaimCaseStatus;
/**
@@ -119,6 +135,9 @@ export class ClaimCase {
@Prop({ type: ClaimEvaluationSchema, default: () => ({}) })
evaluation?: ClaimEvaluation;
@Prop({ type: CaseInquiriesSchema, default: () => ({}) })
inquiries?: CaseInquiries;
/**
* Optional “read-optimized” copy of fields from blame/request side.
* Source of truth remains `blameRequestId`.
@@ -160,4 +179,3 @@ export class ClaimCase {
export type ClaimCaseDocument = HydratedDocument<ClaimCase>;
export const ClaimCaseSchema = SchemaFactory.createForClass(ClaimCase);
ClaimCaseSchema.index({ status: 1, "workflow.locked": 1 });

View File

@@ -0,0 +1,34 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Schema as MongooseSchema } from "mongoose";
@Schema({ _id: false })
export class InquiryStatus {
@Prop({ type: Boolean, default: false })
has?: boolean;
@Prop({ type: MongooseSchema.Types.Mixed, default: () => ({}) })
data?: any;
@Prop({ type: MongooseSchema.Types.Mixed })
error?: any;
@Prop({ type: Date })
updatedAt?: Date;
}
export const InquiryStatusSchema = SchemaFactory.createForClass(InquiryStatus);
@Schema({ _id: false, strict: false })
export class CaseInquiries {
@Prop({ type: InquiryStatusSchema })
thirdParty?: InquiryStatus;
@Prop({ type: InquiryStatusSchema })
carBody?: InquiryStatus;
@Prop({ type: InquiryStatusSchema })
person?: InquiryStatus;
@Prop({ type: InquiryStatusSchema })
drivingLicence?: InquiryStatus;
}
export const CaseInquiriesSchema = SchemaFactory.createForClass(CaseInquiries);

View File

@@ -8,6 +8,10 @@ import { HistoryEvent, HistoryEventSchema } from "./historyEvent.type";
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatus.enum";
import { CreationMethod, FilledBy } from "./request-management.schema";
import {
CaseInquiries,
CaseInquiriesSchema,
} from "src/common/schema/case-inquiries.schema";
/** CAR_BODY only: mocked insurance inquiry result (legacy top-level; prefer parties[].insurance.carBodyInsurance) */
@Schema({ _id: false })
@@ -18,84 +22,93 @@ export class CarBodyInsuranceDetail {
@Prop() insurerCompany?: string;
@Prop({ type: [String] }) coverages?: string[];
}
export const CarBodyInsuranceDetailSchema = SchemaFactory.createForClass(CarBodyInsuranceDetail);
export const CarBodyInsuranceDetailSchema = SchemaFactory.createForClass(
CarBodyInsuranceDetail,
);
@Schema({ _id: false })
export class BlameRequestSnapshot {
@Prop({ type: [PartySchema], default: [] })
parties?: Party[];
@Prop({ type: [PartySchema], default: [] })
parties?: Party[];
}
export const BlameRequestSnapshotSchema =
SchemaFactory.createForClass(BlameRequestSnapshot);
@Schema({ collection: "blameCases", timestamps: true , id:true })
@Schema({ collection: "blameCases", timestamps: true, id: true })
export class BlameRequest {
@Prop({ required: true, unique: true, index: true, trim: true })
requestNo: string;
@Prop({ required: true, unique: true, index: true, trim: true })
requestNo: string;
/**
* Human-friendly shared id across blame+claim (e.g. A14235).
* Generated once at creation time and reused by claim.
*/
@Prop({ required: true, unique: true, index: true, trim: true })
publicId: string;
/**
* Human-friendly shared id across blame+claim (e.g. A14235).
* Generated once at creation time and reused by claim.
*/
@Prop({ required: true, unique: true, index: true, trim: true })
publicId: string;
@Prop({ required: true, type: String, enum: BlameRequestType })
type: BlameRequestType;
@Prop({ required: true, type: String, enum: BlameRequestType })
type: BlameRequestType;
@Prop({ required: true, type: String, enum: CaseStatus, default: CaseStatus.OPEN })
status: CaseStatus;
@Prop({
required: true,
type: String,
enum: CaseStatus,
default: CaseStatus.OPEN,
})
status: CaseStatus;
@Prop({ type: String, enum: BlameStatus, default: BlameStatus.UNKNOWN })
blameStatus: BlameStatus;
@Prop({ type: String, enum: BlameStatus, default: BlameStatus.UNKNOWN })
blameStatus: BlameStatus;
@Prop({ type: WorkflowSchema })
workflow?: Workflow;
@Prop({ type: WorkflowSchema })
workflow?: Workflow;
@Prop({ type: [PartySchema], default: [] })
parties: Party[];
@Prop({ type: [PartySchema], default: [] })
parties: Party[];
@Prop({ type: ExpertSectionSchema })
expert?: ExpertSection;
@Prop({ type: CaseInquiriesSchema, default: () => ({}) })
inquiries?: CaseInquiries;
@Prop({ type: [HistoryEventSchema], default: [] })
history: HistoryEvent[];
@Prop({ type: ExpertSectionSchema })
expert?: ExpertSection;
/** CAR_BODY only: first party car-body insurance (mocked inquiry) legacy; prefer parties[0].insurance.carBodyInsurance */
@Prop({ type: CarBodyInsuranceDetailSchema })
carBodyInsuranceDetail?: CarBodyInsuranceDetail;
@Prop({ type: [HistoryEventSchema], default: [] })
history: HistoryEvent[];
/**
* Optional snapshot structure (kept for future use / read-optimized copies).
* Source of truth remains the top-level fields of this document.
*/
@Prop({ type: BlameRequestSnapshotSchema, required: false })
snapshot?: BlameRequestSnapshot;
/** CAR_BODY only: first party car-body insurance (mocked inquiry) legacy; prefer parties[0].insurance.carBodyInsurance */
@Prop({ type: CarBodyInsuranceDetailSchema })
carBodyInsuranceDetail?: CarBodyInsuranceDetail;
/** True when this blame was created by a field expert (independent expert); only that expert can see/review it. */
@Prop({ default: false })
expertInitiated?: boolean;
/**
* Optional snapshot structure (kept for future use / read-optimized copies).
* Source of truth remains the top-level fields of this document.
*/
@Prop({ type: BlameRequestSnapshotSchema, required: false })
snapshot?: BlameRequestSnapshot;
/** Field expert who created this file (for expert-initiated flows). */
@Prop({ type: Types.ObjectId })
initiatedByFieldExpertId?: Types.ObjectId;
/** True when this blame was created by a field expert (independent expert); only that expert can see/review it. */
@Prop({ default: false })
expertInitiated?: boolean;
/** True when this blame was created by a registrar. */
@Prop({ default: false })
registrarInitiated?: boolean;
/** Field expert who created this file (for expert-initiated flows). */
@Prop({ type: Types.ObjectId })
initiatedByFieldExpertId?: Types.ObjectId;
/** Registrar who created this file (for registrar-initiated flows). */
@Prop({ type: Types.ObjectId })
initiatedByRegistrarId?: Types.ObjectId;
/** True when this blame was created by a registrar. */
@Prop({ default: false })
registrarInitiated?: boolean;
/** LINK = expert sends link to user(s); IN_PERSON = expert fills form on-site. */
@Prop({ type: String, enum: CreationMethod })
creationMethod?: CreationMethod;
/** Registrar who created this file (for registrar-initiated flows). */
@Prop({ type: Types.ObjectId })
initiatedByRegistrarId?: Types.ObjectId;
/** Who fills the blame data: CUSTOMER (LINK) or EXPERT (IN_PERSON). */
@Prop({ type: String, enum: FilledBy })
filledBy?: FilledBy;
/** LINK = expert sends link to user(s); IN_PERSON = expert fills form on-site. */
@Prop({ type: String, enum: CreationMethod })
creationMethod?: CreationMethod;
/** Who fills the blame data: CUSTOMER (LINK) or EXPERT (IN_PERSON). */
@Prop({ type: String, enum: FilledBy })
filledBy?: FilledBy;
}
export type BlameRequestDocument = HydratedDocument<BlameRequest>;

View File

@@ -123,6 +123,64 @@ export class RequestManagementService {
: -1;
}
private normalizeInquiryError(err: any): any {
if (!err) return undefined;
return {
message: err?.message || String(err),
status: err?.response?.status,
data: err?.response?.data,
};
}
private recordCaseInquiryStatus(
req: any,
key: "thirdParty" | "carBody" | "person" | "drivingLicence" | string,
has: boolean,
data: any = {},
error?: any,
): void {
const existingInquiries =
req.inquiries?.toObject?.() ??
(req.inquiries && typeof req.inquiries === "object" ? req.inquiries : {});
req.inquiries = {
...existingInquiries,
[key]: {
has,
data: data ?? {},
...(error ? { error: this.normalizeInquiryError(error) } : {}),
updatedAt: new Date(),
},
};
}
private recordPartyCaseInquiryStatus(
req: any,
key: "thirdParty" | "carBody" | "person" | "drivingLicence" | string,
role: PartyRole,
has: boolean,
data: any = {},
error?: any,
): void {
const existingData =
req?.inquiries?.[key]?.data?.toObject?.() ?? req?.inquiries?.[key]?.data;
const dataByRole =
existingData &&
typeof existingData === "object" &&
!Array.isArray(existingData)
? existingData
: {};
this.recordCaseInquiryStatus(
req,
key,
has,
{
...dataByRole,
[role]: data ?? {},
},
error,
);
}
private assertPartyOwner(party: any, user: any, errMsg: string) {
const partyUserId = party?.person?.userId
? String(party.person.userId)
@@ -967,6 +1025,11 @@ export class RequestManagementService {
this.logger.log(
`[TEJARAT] block inquiry mapped for request=${req._id}: ${JSON.stringify(inquiryMapped)}`,
);
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, true, {
source: "TEJARAT_BLOCK_INQUIRY",
raw: inquiryRaw,
mapped: inquiryMapped,
});
} catch (err: any) {
this.logger.error(
`[TEJARAT] block inquiry failed for request=${req._id}: ${err?.message || err}`,
@@ -978,6 +1041,15 @@ export class RequestManagementService {
}, data=${JSON.stringify(err.response.data)}`,
);
}
this.recordPartyCaseInquiryStatus(
req,
"thirdParty",
role,
false,
{},
err,
);
await (req as any).save();
throw new HttpException("Inquiry failed", HttpStatus.BAD_REQUEST);
}
@@ -987,6 +1059,12 @@ export class RequestManagementService {
inquiryMapped.Error,
)}`,
);
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, false, {
source: "TEJARAT_BLOCK_INQUIRY",
raw: inquiryRaw,
mapped: inquiryMapped,
});
await (req as any).save();
throw new HttpException(
inquiryMapped.Error.Message || "Inquiry returned error",
HttpStatus.BAD_REQUEST,
@@ -1020,6 +1098,13 @@ export class RequestManagementService {
personalNationalCode,
personalBirthDate,
);
this.recordPartyCaseInquiryStatus(
req,
"person",
role,
true,
personalInquiry,
);
this.logger.log(
`[SANDHUB] personal inquiry success request=${req._id} nationalCode=${personalNationalCode}: ${JSON.stringify(
personalInquiry,
@@ -1029,6 +1114,8 @@ export class RequestManagementService {
this.logger.error(
`[SANDHUB] personal inquiry failed request=${req._id} nationalCode=${personalNationalCode}: ${err?.message || err}`,
);
this.recordPartyCaseInquiryStatus(req, "person", role, false, {}, err);
await (req as any).save();
throw new HttpException(
"Personal identity inquiry failed",
HttpStatus.BAD_REQUEST,
@@ -1120,10 +1207,28 @@ export class RequestManagementService {
// CAR BODY EXTERNAL API INQUIRY
if (req.type === BlameRequestType.CAR_BODY && role === PartyRole.FIRST) {
const carBodyInfo = await this.sandHubService.getTejaratCarBodyInquiry({
nationalCodeOfInsurer: body.nationalCodeOfInsurer,
plate: body.plate,
});
let carBodyInfo: any;
try {
carBodyInfo = await this.sandHubService.getTejaratCarBodyInquiry({
nationalCodeOfInsurer: body.nationalCodeOfInsurer,
plate: body.plate,
});
this.recordPartyCaseInquiryStatus(req, "carBody", role, true, {
source: "TEJARAT_CAR_BODY_INQUIRY",
raw: carBodyInfo.raw,
mapped: carBodyInfo.mapped,
});
} catch (err: any) {
this.logger.error(
`[TEJARAT] car body inquiry failed for request=${req._id}: ${err?.message || err}`,
);
this.recordPartyCaseInquiryStatus(req, "carBody", role, false, {}, err);
await (req as any).save();
throw new HttpException(
"Car body inquiry failed",
HttpStatus.BAD_REQUEST,
);
}
// Raw + mapped stored under vehicle (same pattern as third-party inquiry)
party.vehicle.inquiry = {
@@ -4829,8 +4934,24 @@ export class RequestManagementService {
nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer,
});
sandHubReport = sandHubResponse["_doc"] || sandHubResponse;
this.recordPartyCaseInquiryStatus(
req,
"thirdParty",
PartyRole.FIRST,
true,
sandHubReport,
);
} catch (e) {
this.logger.error("SandHub error in expertCompleteCarBodyFormV2:", e);
this.recordPartyCaseInquiryStatus(
req,
"thirdParty",
PartyRole.FIRST,
false,
{},
e,
);
await (req as any).save();
throw new InternalServerErrorException(
"Failed to process plate information.",
);
@@ -4864,6 +4985,13 @@ export class RequestManagementService {
firstPartyPlate.plate,
firstPartyPlate.nationalCodeOfInsurer,
);
this.recordPartyCaseInquiryStatus(
req,
"carBody",
PartyRole.FIRST,
true,
carBodyInfo,
);
const firstParty: any = {
role: PartyRole.FIRST,
@@ -5010,11 +5138,32 @@ export class RequestManagementService {
desc: string,
role: PartyRole,
) => {
const sandHubResponse = await this.sandHubService.getSandHubResponse({
plate: plateDto.plate,
nationalCodeOfInsurer: plateDto.nationalCodeOfInsurer,
});
let sandHubResponse: any;
try {
sandHubResponse = await this.sandHubService.getSandHubResponse({
plate: plateDto.plate,
nationalCodeOfInsurer: plateDto.nationalCodeOfInsurer,
});
} catch (err: any) {
this.recordPartyCaseInquiryStatus(
req,
"thirdParty",
role,
false,
{},
err,
);
await (req as any).save();
throw err;
}
const sandHubReport = (sandHubResponse["_doc"] || sandHubResponse) as any;
this.recordPartyCaseInquiryStatus(
req,
"thirdParty",
role,
true,
sandHubReport,
);
const clientName =
sandHubReport?.CompanyName || sandHubReport?.LastCompanyName;
const companyCode = sandHubReport?.CompanyCode;