forked from Yara724/api
YARA-947, YARA-1038
This commit is contained in:
@@ -49,6 +49,14 @@ export class ExternalInquiryFlagsDto implements ExternalInquiryFlags {
|
||||
})
|
||||
@IsBoolean()
|
||||
carOwnership: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
"ESG VIN/chassis-number inquiry (`/inquiry/policyByChassis`). Required for the VIN initial-form path.",
|
||||
example: false,
|
||||
})
|
||||
@IsBoolean()
|
||||
vinChassis: boolean;
|
||||
}
|
||||
|
||||
export class UpdateClientExternalInquiriesDto extends PartialType(
|
||||
|
||||
@@ -6,6 +6,7 @@ export const EXTERNAL_INQUIRY_TYPES = [
|
||||
"sheba",
|
||||
"drivingLicense",
|
||||
"carOwnership",
|
||||
"vinChassis",
|
||||
] as const;
|
||||
|
||||
export type ExternalInquiryType = (typeof EXTERNAL_INQUIRY_TYPES)[number];
|
||||
@@ -21,6 +22,7 @@ export const DEFAULT_EXTERNAL_INQUIRY_FLAGS: Record<
|
||||
sheba: false,
|
||||
drivingLicense: false,
|
||||
carOwnership: false,
|
||||
vinChassis: false,
|
||||
};
|
||||
|
||||
export type ExternalInquiryFlags = Record<ExternalInquiryType, boolean>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString, MaxLength } from "class-validator";
|
||||
import { Types } from "mongoose";
|
||||
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
|
||||
import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
|
||||
@@ -190,6 +191,52 @@ export class BlameConfessionDtoV2 {
|
||||
imGuilty?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 initial-form step submitted with a VIN/chassis number instead of a plate.
|
||||
* All identity and license fields from {@link AddPlateDto} are preserved; only
|
||||
* `plate` is replaced by `vin` (the 17-character chassis / VIN string).
|
||||
*/
|
||||
export class InitialFormVinDto {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: true,
|
||||
description: "17-character VIN / chassis number (شماره شاسی)",
|
||||
example: "NAAM01E15HK123456",
|
||||
maxLength: 17,
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(17)
|
||||
vin: string;
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
nationalCodeOfInsurer: string;
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
nationalCodeOfDriver: string;
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
insurerLicense: string;
|
||||
|
||||
@ApiProperty({ type: String, required: true })
|
||||
driverLicense: string;
|
||||
|
||||
@ApiProperty({ type: Boolean, required: true })
|
||||
driverIsInsurer: boolean;
|
||||
|
||||
@ApiProperty({ type: Boolean, required: true, default: false })
|
||||
isNewCar: boolean;
|
||||
|
||||
@ApiProperty({ type: Boolean, required: true })
|
||||
userNoCertificate: boolean;
|
||||
|
||||
@ApiProperty({ type: Number, required: true })
|
||||
insurerBirthday: number;
|
||||
|
||||
@ApiPropertyOptional({ type: String, required: false })
|
||||
driverBirthday: string | null;
|
||||
}
|
||||
|
||||
// export class DocsOfThisFile {
|
||||
// @ApiProperty()
|
||||
// firstPartyFile?: FirstPartyFileDto;
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
CarBodyFormDto,
|
||||
CarBodySecondForm,
|
||||
InitialFormDto,
|
||||
InitialFormVinDto,
|
||||
} from "src/request-management/dto/create-request-management.dto";
|
||||
import { BlameVideoDbService } from "src/request-management/entities/db-service/blame-video.db.service";
|
||||
import { BlameVoiceDbService } from "src/request-management/entities/db-service/blame.voice.db.service";
|
||||
@@ -1475,6 +1476,313 @@ export class RequestManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 initial-form submitted with a VIN/chassis number instead of a plate.
|
||||
*
|
||||
* Performs:
|
||||
* 1. ESG `/inquiry/policyByChassis` (or its mock when `vinChassis` is off)
|
||||
* 2. Personal identity inquiry (same as the plate path)
|
||||
*
|
||||
* The inquiry result is mapped through the same `mapEsgPolicyByPlateToOldFormat`
|
||||
* normaliser used by `getTejaratBlockInquiry`, so all downstream party/vehicle
|
||||
* /insurance field assignments are identical.
|
||||
*/
|
||||
async initialFormVinV2(
|
||||
requestId: string,
|
||||
body: InitialFormVinDto,
|
||||
user: any,
|
||||
expectedRole?: string,
|
||||
) {
|
||||
try {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
|
||||
if (!req.workflow?.currentStep) {
|
||||
throw new BadRequestException("Request workflow is not initialized");
|
||||
}
|
||||
|
||||
const stepKey = req.workflow.currentStep as WorkflowStep;
|
||||
if (
|
||||
stepKey !== WorkflowStep.FIRST_INITIAL_FORM &&
|
||||
stepKey !== WorkflowStep.SECOND_INITIAL_FORM
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Invalid step. Expected FIRST_INITIAL_FORM or SECOND_INITIAL_FORM but got ${stepKey}`,
|
||||
);
|
||||
}
|
||||
|
||||
const role = this.stepKeyToPartyRole(stepKey);
|
||||
const idx = this.getPartyIndex(req, role);
|
||||
if (idx === -1)
|
||||
throw new BadRequestException(`${role} party not found on request`);
|
||||
|
||||
const party = req.parties[idx];
|
||||
this.assertPartyOwner(
|
||||
party,
|
||||
user,
|
||||
`Only ${role} party can submit this step`,
|
||||
req,
|
||||
);
|
||||
this.assertExpectedPartyRole(
|
||||
expectedRole,
|
||||
role,
|
||||
this.isBlameOnBehalfActor(req, user),
|
||||
);
|
||||
|
||||
if (!party.person) party.person = {} as any;
|
||||
if (
|
||||
!party.person.userId &&
|
||||
user?.sub &&
|
||||
!this.isBlameOnBehalfActor(req, user)
|
||||
) {
|
||||
party.person.userId = Types.ObjectId.isValid(user.sub)
|
||||
? new Types.ObjectId(user.sub)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// Validation: driver/insurer sameness rules (identical to plate path)
|
||||
if (body.driverIsInsurer === false) {
|
||||
if (
|
||||
body.nationalCodeOfInsurer === body.nationalCodeOfDriver ||
|
||||
body.insurerLicense === body.driverLicense
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Insurer and Driver should be two different persons in this mode.",
|
||||
);
|
||||
}
|
||||
} else if (body.driverIsInsurer === true) {
|
||||
const sameNat =
|
||||
body.nationalCodeOfInsurer === body.nationalCodeOfDriver;
|
||||
const sameLic = body.insurerLicense === body.driverLicense;
|
||||
const sameBirthday =
|
||||
body.driverBirthday == null ||
|
||||
String(body.driverBirthday) === String(body.insurerBirthday);
|
||||
if (!sameNat || !sameLic || !sameBirthday) {
|
||||
throw new BadRequestException(
|
||||
"When driverIsInsurer is true, insurer and driver data must be the same.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- External inquiry: ESG policyByChassis ----
|
||||
const inquiryClientId = party.person?.clientId
|
||||
? String(party.person.clientId)
|
||||
: undefined;
|
||||
const inquiryOptions = inquiryClientId
|
||||
? { clientId: inquiryClientId }
|
||||
: undefined;
|
||||
|
||||
let inquiryRaw: any;
|
||||
let inquiryMapped: any;
|
||||
try {
|
||||
const inquiry = await this.sandHubService.getPolicyByChassisInquiry(
|
||||
body.vin,
|
||||
inquiryOptions,
|
||||
);
|
||||
inquiryRaw = inquiry.raw;
|
||||
inquiryMapped = inquiry.mapped;
|
||||
this.logger.log(
|
||||
`[ESG] policyByChassis raw for request=${req._id}: ${JSON.stringify(inquiryRaw)}`,
|
||||
);
|
||||
this.logger.log(
|
||||
`[ESG] policyByChassis mapped for request=${req._id}: ${JSON.stringify(inquiryMapped)}`,
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, true, {
|
||||
source: "ESG_VIN_INQUIRY",
|
||||
raw: inquiryRaw,
|
||||
mapped: inquiryMapped,
|
||||
});
|
||||
} catch (err: any) {
|
||||
this.logger.error(
|
||||
`[ESG] policyByChassis failed for request=${req._id}: ${err?.message || err}`,
|
||||
);
|
||||
if (err?.response) {
|
||||
this.logger.error(
|
||||
`[ESG] policyByChassis response for request=${req._id}: status=${
|
||||
err.response.status
|
||||
}, data=${JSON.stringify(err.response.data)}`,
|
||||
);
|
||||
}
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
"thirdParty",
|
||||
role,
|
||||
false,
|
||||
{},
|
||||
err,
|
||||
);
|
||||
await this.blameRequestDbService.findByIdAndUpdate(req._id, {
|
||||
$set: { inquiries: req.inquiries },
|
||||
});
|
||||
throw new HttpException("VIN inquiry failed", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (inquiryMapped?.Error) {
|
||||
this.logger.warn(
|
||||
`[ESG] policyByChassis error for request=${req._id}: ${JSON.stringify(inquiryMapped.Error)}`,
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(req, "thirdParty", role, false, {
|
||||
source: "ESG_VIN_INQUIRY",
|
||||
raw: inquiryRaw,
|
||||
mapped: inquiryMapped,
|
||||
});
|
||||
await this.blameRequestDbService.findByIdAndUpdate(req._id, {
|
||||
$set: { inquiries: req.inquiries },
|
||||
});
|
||||
throw new HttpException(
|
||||
inquiryMapped.Error.Message || "VIN inquiry returned error",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// ---- External inquiry 2: personal identity (same as plate path) ----
|
||||
const personalNationalCode =
|
||||
body.nationalCodeOfInsurer || body.nationalCodeOfDriver;
|
||||
const personalBirthDate: number | string | null = (body.insurerBirthday ??
|
||||
body.driverBirthday ??
|
||||
null) as number | string | null;
|
||||
if (
|
||||
!personalNationalCode ||
|
||||
personalBirthDate === null ||
|
||||
personalBirthDate === undefined ||
|
||||
String(personalBirthDate).trim() === ""
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Valid nationalCode and birthDate are required for personal inquiry.",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const personalInquiry = await this.sandHubService.getPersonalInquiry(
|
||||
personalNationalCode,
|
||||
personalBirthDate,
|
||||
inquiryOptions,
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(
|
||||
req,
|
||||
"person",
|
||||
role,
|
||||
true,
|
||||
personalInquiry,
|
||||
);
|
||||
this.logger.log(
|
||||
`[SANDHUB] personal inquiry success request=${req._id} nationalCode=${personalNationalCode}: ${JSON.stringify(personalInquiry)}`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.error(
|
||||
`[SANDHUB] personal inquiry failed request=${req._id} nationalCode=${personalNationalCode}: ${err?.message || err}`,
|
||||
);
|
||||
this.recordPartyCaseInquiryStatus(req, "person", role, false, {}, err);
|
||||
await this.blameRequestDbService.findByIdAndUpdate(req._id, {
|
||||
$set: { inquiries: req.inquiries },
|
||||
});
|
||||
throw new HttpException(
|
||||
"Personal identity inquiry failed",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve insurer client from inquiry response
|
||||
const clientName = inquiryMapped?.CompanyName;
|
||||
if (!clientName) {
|
||||
throw new BadRequestException(
|
||||
"CompanyName missing from VIN inquiry response",
|
||||
);
|
||||
}
|
||||
const companyCode = inquiryMapped?.CompanyCode;
|
||||
const client = await this.clientService.findOrCreateClientByCompanyCode(
|
||||
companyCode,
|
||||
clientName,
|
||||
);
|
||||
if (!client) {
|
||||
throw new BadRequestException(
|
||||
"CompanyCode missing or invalid in VIN inquiry response",
|
||||
);
|
||||
}
|
||||
|
||||
// Persist party data
|
||||
const resolvedClientId =
|
||||
(client as any)?._id ?? (client as any)?._doc?._id;
|
||||
party.person.clientId = resolvedClientId;
|
||||
party.person.nationalCodeOfInsurer = body.nationalCodeOfInsurer;
|
||||
party.person.nationalCodeOfDriver = body.nationalCodeOfDriver;
|
||||
party.person.insurerLicense = body.insurerLicense;
|
||||
party.person.driverLicense = body.driverLicense;
|
||||
party.person.driverIsInsurer = body.driverIsInsurer;
|
||||
party.person.isNewCar = body.isNewCar;
|
||||
party.person.userNoCertificate = body.userNoCertificate;
|
||||
party.person.insurerBirthday = body.insurerBirthday;
|
||||
party.person.driverBirthday =
|
||||
body.driverBirthday ??
|
||||
(body.driverIsInsurer ? String(body.insurerBirthday) : null);
|
||||
|
||||
if (!party.vehicle) party.vehicle = {} as any;
|
||||
party.vehicle.isNew = body.isNewCar;
|
||||
// Store VIN as the vehicle identifier instead of plate
|
||||
party.vehicle.plateId = body.vin;
|
||||
party.vehicle.name = inquiryMapped?.MapTypNam;
|
||||
party.vehicle.type = `${inquiryMapped?.UsageField} / ${inquiryMapped?.MapUsageName || "-"}`;
|
||||
party.vehicle.inquiry = {
|
||||
source: "ESG_VIN_INQUIRY",
|
||||
raw: inquiryRaw,
|
||||
mapped: inquiryMapped,
|
||||
};
|
||||
|
||||
if (!party.insurance) party.insurance = {} as any;
|
||||
party.insurance.policyNumber = inquiryMapped?.PrntPlcyCmpDocNo;
|
||||
party.insurance.company = clientName;
|
||||
party.insurance.financialCeiling = inquiryMapped?.FinancialCvrCptl;
|
||||
party.insurance.startDate = inquiryMapped?.IssueDate;
|
||||
party.insurance.endDate = inquiryMapped?.EndDate;
|
||||
|
||||
// Advance workflow
|
||||
await this.advanceWorkflowToNext(req, stepKey);
|
||||
|
||||
const historyEntry: any = {
|
||||
type: `${stepKey}_SUBMITTED`,
|
||||
actor: {
|
||||
actorId: Types.ObjectId.isValid(user?.sub)
|
||||
? new Types.ObjectId(user.sub)
|
||||
: undefined,
|
||||
actorName: user?.fullName,
|
||||
actorType: "user",
|
||||
},
|
||||
metadata: {
|
||||
role,
|
||||
companyCode,
|
||||
companyName: clientName,
|
||||
inquiryType: "VIN",
|
||||
vin: body.vin,
|
||||
},
|
||||
};
|
||||
|
||||
await this.blameRequestDbService.findByIdAndUpdate(req._id, {
|
||||
$set: {
|
||||
[`parties.${idx}.person`]: party.person,
|
||||
[`parties.${idx}.vehicle`]: party.vehicle,
|
||||
[`parties.${idx}.insurance`]: party.insurance,
|
||||
inquiries: req.inquiries,
|
||||
"workflow.completedSteps": req.workflow.completedSteps,
|
||||
"workflow.currentStep": req.workflow.currentStep,
|
||||
"workflow.nextStep": req.workflow.nextStep,
|
||||
status: req.status,
|
||||
},
|
||||
$push: { history: historyEntry },
|
||||
});
|
||||
|
||||
return {
|
||||
requestId: req._id,
|
||||
publicId: req.publicId,
|
||||
workflow: req.workflow,
|
||||
blameStatus: req.blameStatus,
|
||||
status: req.status,
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async addDetailLocationV2(
|
||||
requestId: string,
|
||||
body: LocationDto,
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
BlameConfessionDtoV2,
|
||||
CreateBlameRequestDtoV2,
|
||||
DescriptionDto,
|
||||
InitialFormVinDto,
|
||||
LocationDto,
|
||||
CarBodyFormDto,
|
||||
} from "./dto/create-request-management.dto";
|
||||
@@ -197,6 +198,26 @@ export class RequestManagementV2Controller {
|
||||
return this.requestManagementService.initialFormV2(requestId, body, user);
|
||||
}
|
||||
|
||||
@Post("/initial-form-vin/:requestId")
|
||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||
@ApiBody({
|
||||
type: InitialFormVinDto,
|
||||
description:
|
||||
"Same identity/license fields as the plate form, but with `vin` (17-char chassis number) instead of `plate`.",
|
||||
})
|
||||
@UseGuards(GlobalGuard)
|
||||
async initialFormVinV2(
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() body: InitialFormVinDto,
|
||||
@CurrentUser() user,
|
||||
) {
|
||||
return this.requestManagementService.initialFormVinV2(
|
||||
requestId,
|
||||
body,
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("/add-detail-location/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: LocationDto })
|
||||
|
||||
@@ -893,6 +893,48 @@ export class SandHubService {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ESG VIN/chassis-number inquiry (`/inquiry/policyByChassis`).
|
||||
*
|
||||
* When `vinChassis` inquiry is disabled (mock mode) the response shape mirrors
|
||||
* `buildMockPlateInquiryRaw` so the downstream mapper (`mapEsgPolicyByPlateToOldFormat`)
|
||||
* can normalise it into the same `{ raw, mapped }` contract used by the plate path.
|
||||
*
|
||||
* @param chassisNo - 17-character VIN / chassis number
|
||||
* @param options - optional per-tenant client scope
|
||||
*/
|
||||
async getPolicyByChassisInquiry(
|
||||
chassisNo: string,
|
||||
options?: SandHubInquiryOptions,
|
||||
): Promise<{ raw: any; mapped: any }> {
|
||||
const baseUrl = process.env.ESG_URL ?? "http://192.168.20.22:8085";
|
||||
const requestUrl = `${baseUrl}/inquiry/policyByChassis`;
|
||||
const requestPayload = { chassisNo };
|
||||
|
||||
const live = await this.isInquiryLive("vinChassis", options);
|
||||
|
||||
if (!live) {
|
||||
const ctx = await this.mockCompanyContext(options);
|
||||
const raw = this.buildMockPlateInquiryRaw(ctx);
|
||||
this.logger.debug(
|
||||
`[MOCK] getPolicyByChassisInquiry chassisNo=${chassisNo}`,
|
||||
);
|
||||
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw);
|
||||
return { raw, mapped };
|
||||
}
|
||||
|
||||
const raw = await this.makeEsgRequest(
|
||||
requestUrl,
|
||||
requestPayload,
|
||||
"vinChassis",
|
||||
options,
|
||||
);
|
||||
const mapped = this.mapEsgPolicyByPlateToOldFormat(raw);
|
||||
return { raw, mapped };
|
||||
}
|
||||
|
||||
|
||||
private async makeSandHubRequest(
|
||||
url: string,
|
||||
payload: any,
|
||||
|
||||
Reference in New Issue
Block a user