1
0
forked from Yara724/api

Merge pull request 'main' (#166) from s.hajizadeh/yara724api:main into main

Reviewed-on: Yara724/api#166
This commit is contained in:
2026-07-01 15:14:56 +03:30
16 changed files with 2366 additions and 146 deletions

1
package-lock.json generated
View File

@@ -23,6 +23,7 @@
"class-validator": "^0.15.1",
"express": "^5.2.1",
"fastest-levenshtein": "^1.0.16",
"form-data": "^4.0.6",
"joi": "^18.2.1",
"mongoose": "^8.9.2",
"pdfkit": "^0.19.1",

View File

@@ -39,6 +39,7 @@
"class-validator": "^0.15.1",
"express": "^5.2.1",
"fastest-levenshtein": "^1.0.16",
"form-data": "^4.0.6",
"joi": "^18.2.1",
"mongoose": "^8.9.2",
"pdfkit": "^0.19.1",

View File

@@ -54,11 +54,13 @@ import { JwtModule } from "@nestjs/jwt";
import { MediaPolicyModule } from "src/media-policy/media-policy.module";
import { HttpModule } from "@nestjs/axios";
import { FanavaranAuditModule } from "src/fanavaran/fanavaran-audit.module";
import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module";
@Module({
imports: [
HttpModule,
FanavaranAuditModule,
FanavaranLookupModule,
PublicIdModule,
UsersModule,
RequestManagementModule,

View File

@@ -1,34 +1,34 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEnum, IsNotEmpty, IsString } from "class-validator";
/**
* V2 DTO for capturing car angle or damaged part
*/
export class CapturePartV2Dto {
@ApiProperty({
description: 'Type of capture: angle or part',
example: 'angle',
enum: ['angle', 'part'],
description: "Type of capture: angle or part",
example: "angle",
enum: ["angle", "part"],
})
@IsNotEmpty({ message: 'Capture type is required' })
@IsEnum(['angle', 'part'], {
@IsNotEmpty({ message: "Capture type is required" })
@IsEnum(["angle", "part"], {
message: 'Capture type must be either "angle" or "part"',
})
captureType: 'angle' | 'part';
captureType: "angle" | "part";
@ApiProperty({
description:
'When captureType is angle: front | back | left | right. When part: catalog id as string (e.g. "101"), 0-based index (e.g. "0"), or full catalog key (e.g. left_backfender). Prefer id or index for parts.',
example: 'front',
example: "front",
})
@IsNotEmpty({ message: 'Capture key is required' })
@IsString({ message: 'Capture key must be a string' })
@IsNotEmpty({ message: "Capture key is required" })
@IsString({ message: "Capture key must be a string" })
captureKey: string;
@ApiProperty({
type: 'string',
format: 'binary',
description: 'Image file (JPG, PNG)',
type: "string",
format: "binary",
description: "Image file (JPG, PNG)",
})
file: Express.Multer.File;
}
@@ -38,51 +38,58 @@ export class CapturePartV2Dto {
*/
export class CapturePartV2ResponseDto {
@ApiProperty({
description: 'Claim request ID',
example: '507f1f77bcf86cd799439011',
description: "Claim request ID",
example: "507f1f77bcf86cd799439011",
})
claimRequestId: string;
@ApiProperty({
description: 'Type of capture',
example: 'angle',
description: "Type of capture",
example: "angle",
})
captureType: string;
@ApiProperty({
description: 'Key of what was captured',
example: 'front',
description: "Key of what was captured",
example: "front",
})
captureKey: string;
@ApiProperty({
description: 'File URL',
example: 'http://localhost:3000/files/captures/front-1234567890.jpg',
description: "File URL",
example: "http://localhost:3000/files/captures/front-1234567890.jpg",
})
fileUrl: string;
@ApiProperty({
description: 'Whether all captures are now complete',
description: "Whether all captures are now complete",
example: false,
})
allCapturesComplete: boolean;
@ApiProperty({
description: 'Current workflow step',
example: 'CAPTURE_PART_DAMAGES',
description: "Current workflow step",
example: "CAPTURE_PART_DAMAGES",
})
currentStep: string;
@ApiProperty({
description: 'Success message',
example: 'Angle captured successfully. 6 captures remaining.',
description: "Success message",
example: "Angle captured successfully. 6 captures remaining.",
})
message: string;
@ApiPropertyOptional({
description: 'True when expert-requested part resends are complete and the claim returned to the expert queue.',
description:
"True when expert-requested part resends are complete and the claim returned to the expert queue.",
})
expertResendComplete?: boolean;
@ApiPropertyOptional({
description:
"Best-effort Fanavaran attachment upload result. Local capture still succeeds when this contains a warning.",
})
fanavaranAttachment?: unknown;
}
/**
@@ -90,20 +97,20 @@ export class CapturePartV2ResponseDto {
*/
export class VideoCaptureV2ResponseDto {
@ApiProperty({
description: 'Claim case ID',
example: '507f1f77bcf86cd799439011',
description: "Claim case ID",
example: "507f1f77bcf86cd799439011",
})
claimRequestId: string;
@ApiProperty({
description: 'ID of the stored video document (claim-video-capture)',
example: '507f1f77bcf86cd799439012',
description: "ID of the stored video document (claim-video-capture)",
example: "507f1f77bcf86cd799439012",
})
videoId: string;
@ApiProperty({
description: 'Success message',
example: 'Video capture uploaded successfully.',
description: "Success message",
example: "Video capture uploaded successfully.",
})
message: string;
}

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
export class CreateClaimFromBlameResponseDto {
@ApiProperty({
@@ -23,4 +23,10 @@ export class CreateClaimFromBlameResponseDto {
example: "Claim request created successfully",
})
message: string;
@ApiPropertyOptional({
description:
"Best-effort Fanavaran early submit result. Claim creation still succeeds when this contains a warning.",
})
fanavaran?: unknown;
}

View File

@@ -1,5 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsEnum, IsNotEmpty, ArrayMinSize, ArrayUnique, IsOptional, IsInt } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import {
IsArray,
IsEnum,
IsNotEmpty,
ArrayMinSize,
ArrayUnique,
IsOptional,
IsInt,
} from "class-validator";
import {
ClaimVehicleTypeV2,
OuterPartSideV2,
@@ -12,27 +20,27 @@ import { DamageSelectedPartV2BodyDto } from "./damage-selected-part-v2.dto";
*/
export enum OuterCarPart {
// Hood
HOOD = 'hood',
HOOD = "hood",
// Doors
FRONT_RIGHT_DOOR = 'front_right_door',
FRONT_LEFT_DOOR = 'front_left_door',
REAR_RIGHT_DOOR = 'rear_right_door',
REAR_LEFT_DOOR = 'rear_left_door',
FRONT_RIGHT_DOOR = "front_right_door",
FRONT_LEFT_DOOR = "front_left_door",
REAR_RIGHT_DOOR = "rear_right_door",
REAR_LEFT_DOOR = "rear_left_door",
// Bumpers
FRONT_BUMPER = 'front_bumper',
REAR_BUMPER = 'rear_bumper',
FRONT_BUMPER = "front_bumper",
REAR_BUMPER = "rear_bumper",
// Fenders
FRONT_RIGHT_FENDER = 'front_right_fender',
FRONT_LEFT_FENDER = 'front_left_fender',
REAR_RIGHT_FENDER = 'rear_right_fender',
REAR_LEFT_FENDER = 'rear_left_fender',
FRONT_RIGHT_FENDER = "front_right_fender",
FRONT_LEFT_FENDER = "front_left_fender",
REAR_RIGHT_FENDER = "rear_right_fender",
REAR_LEFT_FENDER = "rear_left_fender",
// Trunk & Roof
TRUNK = 'trunk',
ROOF = 'roof',
TRUNK = "trunk",
ROOF = "roof",
}
/**
@@ -41,38 +49,38 @@ export enum OuterCarPart {
*/
export class SelectOuterPartsV2Dto {
@ApiProperty({
description: 'Array of selected damaged outer car parts',
example: ['hood', 'front_right_door', 'rear_bumper', 'roof'],
description: "Array of selected damaged outer car parts",
example: ["hood", "front_right_door", "rear_bumper", "roof"],
enum: OuterCarPart,
isArray: true,
minItems: 1,
maxItems: 13,
})
@IsOptional()
@IsArray({ message: 'selectedParts must be an array' })
@ArrayMinSize(1, { message: 'At least one damaged part must be selected' })
@ArrayUnique({ message: 'Duplicate parts are not allowed' })
@IsArray({ message: "selectedParts must be an array" })
@ArrayMinSize(1, { message: "At least one damaged part must be selected" })
@ArrayUnique({ message: "Duplicate parts are not allowed" })
@IsEnum(OuterCarPart, {
each: true,
message: 'Invalid part name. Must be one of the valid outer car parts',
message: "Invalid part name. Must be one of the valid outer car parts",
})
selectedParts?: OuterCarPart[];
@ApiProperty({
description: 'Selected outer part IDs from catalog',
description: "Selected outer part IDs from catalog",
example: [9, 10, 4],
type: [Number],
required: false,
})
@IsOptional()
@IsArray({ message: 'selectedPartIds must be an array' })
@ArrayMinSize(1, { message: 'At least one part ID must be selected' })
@ArrayUnique({ message: 'Duplicate part IDs are not allowed' })
@IsInt({ each: true, message: 'Each selected part ID must be an integer' })
@IsArray({ message: "selectedPartIds must be an array" })
@ArrayMinSize(1, { message: "At least one part ID must be selected" })
@ArrayUnique({ message: "Duplicate part IDs are not allowed" })
@IsInt({ each: true, message: "Each selected part ID must be an integer" })
selectedPartIds?: number[];
@ApiProperty({
description: 'Vehicle type for validating available outer parts',
description: "Vehicle type for validating available outer parts",
enum: ClaimVehicleTypeV2,
required: true,
})
@@ -86,14 +94,14 @@ export class SelectOuterPartsV2Dto {
*/
export class SelectOuterPartsV2ResponseDto {
@ApiProperty({
description: 'Claim request ID',
example: '507f1f77bcf86cd799439011',
description: "Claim request ID",
example: "507f1f77bcf86cd799439011",
})
claimRequestId: string;
@ApiProperty({
description: 'Public ID shared across blame and claim',
example: 'A14235',
description: "Public ID shared across blame and claim",
example: "A14235",
})
publicId: string;
@@ -105,29 +113,36 @@ export class SelectOuterPartsV2ResponseDto {
selectedParts: DamageSelectedPartV2BodyDto[];
@ApiProperty({
description: 'Selected part IDs',
description: "Selected part IDs",
example: [9, 7, 11],
type: [Number],
})
selectedPartIds: number[];
@ApiProperty({
description: 'Current workflow step',
example: 'SELECT_OUTER_PARTS',
description: "Current workflow step",
example: "SELECT_OUTER_PARTS",
})
currentStep: string;
@ApiProperty({
description: 'Next possible workflow step',
example: 'SELECT_OTHER_PARTS',
description: "Next possible workflow step",
example: "SELECT_OTHER_PARTS",
})
nextStep: string;
@ApiProperty({
description: 'Success message',
example: 'Outer parts selected successfully. Please proceed to select other parts.',
description: "Success message",
example:
"Outer parts selected successfully. Please proceed to select other parts.",
})
message: string;
@ApiPropertyOptional({
description:
"Best-effort Fanavaran damage-case submit result. Selecting parts still succeeds when this contains a warning.",
})
fanavaranDamageCase?: unknown;
}
export class SetClaimVehicleTypeV2Dto {

View File

@@ -1,26 +1,26 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { ClaimRequiredDocumentType } from 'src/Types&Enums/claim-request-management/required-document-type.enum';
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEnum, IsNotEmpty, IsString } from "class-validator";
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
/**
* V2 DTO for uploading required document
*/
export class UploadRequiredDocumentV2Dto {
@ApiProperty({
description: 'Document type/key',
example: 'car_green_card',
description: "Document type/key",
example: "car_green_card",
enum: ClaimRequiredDocumentType,
})
@IsNotEmpty({ message: 'Document key is required' })
@IsNotEmpty({ message: "Document key is required" })
@IsEnum(ClaimRequiredDocumentType, {
message: 'Invalid document type',
message: "Invalid document type",
})
documentKey: ClaimRequiredDocumentType;
@ApiProperty({
type: 'string',
format: 'binary',
description: 'Image file (JPG, PNG, PDF)',
type: "string",
format: "binary",
description: "Image file (JPG, PNG, PDF)",
})
file: Express.Multer.File;
}
@@ -30,43 +30,51 @@ export class UploadRequiredDocumentV2Dto {
*/
export class UploadRequiredDocumentV2ResponseDto {
@ApiProperty({
description: 'Claim request ID',
example: '507f1f77bcf86cd799439011',
description: "Claim request ID",
example: "507f1f77bcf86cd799439011",
})
claimRequestId: string;
@ApiProperty({
description: 'Document key that was uploaded',
example: 'car_green_card',
description: "Document key that was uploaded",
example: "car_green_card",
})
documentKey: string;
@ApiProperty({
description: 'File URL',
example: 'http://localhost:3000/files/documents/car-green-card-1234567890.jpg',
description: "File URL",
example:
"http://localhost:3000/files/documents/car-green-card-1234567890.jpg",
})
fileUrl: string;
@ApiProperty({
description: 'Whether all required documents are now uploaded',
description: "Whether all required documents are now uploaded",
example: false,
})
allDocumentsUploaded: boolean;
@ApiProperty({
description: 'Current workflow step',
example: 'UPLOAD_REQUIRED_DOCUMENTS',
description: "Current workflow step",
example: "UPLOAD_REQUIRED_DOCUMENTS",
})
currentStep: string;
@ApiProperty({
description: 'Success message',
example: 'Document uploaded successfully. 12 documents remaining.',
description: "Success message",
example: "Document uploaded successfully. 12 documents remaining.",
})
message: string;
@ApiPropertyOptional({
description: 'True when the owner finished every damage-expert resend requirement and the claim is back in the expert queue.',
description:
"True when the owner finished every damage-expert resend requirement and the claim is back in the expert queue.",
})
expertResendComplete?: boolean;
@ApiPropertyOptional({
description:
"Best-effort Fanavaran attachment upload result. Local document upload still succeeds when this contains a warning.",
})
fanavaranAttachment?: unknown;
}

View File

@@ -58,6 +58,55 @@ export class RequiredDocumentRef {
export const RequiredDocumentRefSchema =
SchemaFactory.createForClass(RequiredDocumentRef);
@Schema({ _id: false })
export class FanavaranSyncStage {
@Prop({ type: String })
status?: "pending" | "success" | "failed" | "skipped";
@Prop({ type: Date })
lastTriedAt?: Date;
@Prop({ type: String })
lastError?: string;
@Prop({ type: Number })
claimId?: number;
@Prop({ type: Number })
claimNo?: number;
@Prop({ type: Number })
dmgCaseId?: number;
@Prop({ type: Number })
expertiseId?: number;
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
files?: unknown[];
@Prop({ type: MongooseSchema.Types.Mixed })
response?: unknown;
}
export const FanavaranSyncStageSchema =
SchemaFactory.createForClass(FanavaranSyncStage);
@Schema({ _id: false })
export class FanavaranSyncState {
@Prop({ type: FanavaranSyncStageSchema })
baseClaim?: FanavaranSyncStage;
@Prop({ type: FanavaranSyncStageSchema })
damageCase?: FanavaranSyncStage;
@Prop({ type: FanavaranSyncStageSchema })
attachments?: FanavaranSyncStage;
@Prop({ type: FanavaranSyncStageSchema })
expertise?: FanavaranSyncStage;
}
export const FanavaranSyncStateSchema =
SchemaFactory.createForClass(FanavaranSyncState);
@Schema({
collection: "claimCases",
timestamps: true,
@@ -142,6 +191,15 @@ export class ClaimCase {
@Prop({ type: Number })
claimId?: number;
@Prop({ type: Number })
dmgCaseId?: number;
@Prop({ type: Number })
expertiseId?: number;
@Prop({ type: FanavaranSyncStateSchema, default: () => ({}) })
fanavaranSync?: FanavaranSyncState;
@Prop({ type: ClaimDamageSelectionSchema, default: () => ({}) })
damage?: ClaimDamageSelection;

View File

@@ -5,7 +5,9 @@ export const FANAVARAN_CLIENT_KEYS: readonly FanavaranClientKey[] = [
"tejaratno",
] as const;
export function isFanavaranClientKey(value: string): value is FanavaranClientKey {
export function isFanavaranClientKey(
value: string,
): value is FanavaranClientKey {
const normalized = value?.trim().toLowerCase();
return normalized === "parsian" || normalized === "tejaratno";
}
@@ -38,6 +40,13 @@ export interface FanavaranPayloadDefaults {
CompensationReferenceId: number;
CulpritLicenceTypeId: number;
CulpritTypeId: number;
DmgCaseTypeId: number;
DmgHistoryStatus: number;
PlaqueKindId: number;
PlaqueSampleId: number;
DriverIsOwner: number;
FaultPercent: number;
ClaimFileTypeId: number;
}
export interface FanavaranClientProfile {
@@ -69,6 +78,13 @@ const FANAVARAN_CLIENT_PROFILES: Record<
CompensationReferenceId: 167,
CulpritLicenceTypeId: 2,
CulpritTypeId: 337,
DmgCaseTypeId: 175,
DmgHistoryStatus: 5214,
PlaqueKindId: 8,
PlaqueSampleId: 10,
DriverIsOwner: 0,
FaultPercent: 100,
ClaimFileTypeId: 70,
},
},
parsian: {
@@ -90,6 +106,13 @@ const FANAVARAN_CLIENT_PROFILES: Record<
CompensationReferenceId: 167,
CulpritLicenceTypeId: 2,
CulpritTypeId: 337,
DmgCaseTypeId: 175,
DmgHistoryStatus: 5214,
PlaqueKindId: 8,
PlaqueSampleId: 10,
DriverIsOwner: 0,
FaultPercent: 100,
ClaimFileTypeId: 70,
},
},
};
@@ -127,14 +150,14 @@ export function fanavaranPreviewPath(
clientKey: FanavaranClientKey,
claimCaseId: string,
): string {
return `/v2/fanavaran/${clientKey}/claim-cases/${claimCaseId}`;
return `/v2/fanavaran/${clientKey}/claim-cases/${claimCaseId}/base-claim/preview`;
}
export function fanavaranSubmitPath(
clientKey: FanavaranClientKey,
claimCaseId: string,
): string {
return `/v2/fanavaran/${clientKey}/claim-cases/${claimCaseId}`;
return `/v2/fanavaran/${clientKey}/claim-cases/${claimCaseId}/base-claim/submit`;
}
export function fanavaranManualSubmitPath(claimCaseId: string): string {

View File

@@ -19,6 +19,7 @@ import { ClaimRequestManagementDbService } from "src/claim-request-management/en
import {
ClaimRequestManagementService,
FanavaranAutoSubmitResult,
FanavaranExpertiseSubmitResult,
} from "src/claim-request-management/claim-request-management.service";
import { ClaimSignDbService } from "src/claim-request-management/entites/db-service/claim-sign.db.service";
import { DamageImageDbService } from "src/claim-request-management/entites/db-service/damage-image.db.service";
@@ -289,7 +290,7 @@ export class ExpertClaimService {
private appendFanavaranAutoSubmitToMessage(
baseMessage: string,
fanavaran: FanavaranAutoSubmitResult,
fanavaran: FanavaranAutoSubmitResult | FanavaranExpertiseSubmitResult,
): string {
if (fanavaran.submitted) {
return `${baseMessage} The claim was sent to Fanavaran successfully.`;
@@ -2371,16 +2372,24 @@ export class ExpertClaimService {
await this.claimRequestManagementService.autoSubmitToFanavaranV2OnClaimCompleted(
claimRequestId,
);
const fanavaranExpertise =
await this.claimRequestManagementService.autoSubmitFanavaranExpertiseOnExpertReply(
claimRequestId,
);
return {
message: this.appendFanavaranAutoSubmitToMessage(
"Factors were reviewed with expert repricing on rejected lines. The claim is completed without an owner signature (temporary policy; may require owner acceptance later).",
fanavaran,
this.appendFanavaranAutoSubmitToMessage(
"Factors were reviewed with expert repricing on rejected lines. The claim is completed without an owner signature (temporary policy; may require owner acceptance later).",
fanavaran,
),
fanavaranExpertise,
),
claimRequestId,
claimStatus: ClaimStatus.APPROVED,
caseStatus: ClaimCaseStatus.COMPLETED,
outcome: "REJECTED_REPRICED_AUTO_COMPLETED",
fanavaran,
fanavaranExpertise,
};
}
@@ -2397,17 +2406,25 @@ export class ExpertClaimService {
await this.claimRequestManagementService.autoSubmitToFanavaranV2OnClaimCompleted(
claimRequestId,
);
const fanavaranExpertise =
await this.claimRequestManagementService.autoSubmitFanavaranExpertiseOnExpertReply(
claimRequestId,
);
return {
message: this.appendFanavaranAutoSubmitToMessage(
"All factors were approved by the expert. The claim is completed without an additional owner signature.",
fanavaran,
this.appendFanavaranAutoSubmitToMessage(
"All factors were approved by the expert. The claim is completed without an additional owner signature.",
fanavaran,
),
fanavaranExpertise,
),
claimRequestId,
claimStatus: ClaimStatus.APPROVED,
caseStatus: ClaimCaseStatus.COMPLETED,
outcome: "ALL_APPROVED_AUTO_COMPLETED",
fanavaran,
fanavaranExpertise,
};
}
@@ -3239,6 +3256,18 @@ export class ExpertClaimService {
});
}
const fanavaranExpertise = !needsFactorUpload
? await this.claimRequestManagementService.autoSubmitFanavaranExpertiseOnExpertReply(
claimRequestId,
)
: {
attempted: false,
submitted: false,
skipped: true,
skipReason:
"Expertise submit waits until factor-needed repair lines are validated.",
};
return {
claimRequestId,
status: nextCaseStatus,
@@ -3251,6 +3280,7 @@ export class ExpertClaimService {
mixedPricingAndFactors: mixedFactorAndPrice,
allPartsFactorNeeded: !!needsFactorUpload && !!allFactorLines,
isFinalReplyAfterObjection,
fanavaranExpertise,
};
}

View File

@@ -41,6 +41,31 @@ export const FANAVARAN_REMOTE_LOOKUPS: FanavaranRemoteLookupDefinition[] = [
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/accident-culprit-type`,
cacheFile: "accident-culprit-type.json",
},
{
name: "inspection-place",
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/inspection-place`,
cacheFile: "inspection-place.json",
},
{
name: "drop-amount-status",
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/drop-amount-status`,
cacheFile: "drop-amount-status.json",
},
{
name: "car-components",
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/base-info/car-components`,
cacheFile: "car-components.json",
},
{
name: "accident-level",
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/accident-level`,
cacheFile: "accident-level.json",
},
{
name: "expert-status",
url: `${FANAVARAN_LOOKUP_BASE_URL}/car/code-list/expert-status`,
cacheFile: "expert-status.json",
},
];
export const TEJARAT_STATIC_ACCIDENT_FILES = {

View File

@@ -54,11 +54,11 @@ export class FanavaranController {
};
}
@Get(":client/claim-cases/:claimCaseId")
@Get(":client/claim-cases/:claimCaseId/base-claim/preview")
@ApiOperation({
summary: "Preview Fanavaran submit payload (V2)",
summary: "Preview Fanavaran base claim create payload",
description:
"Builds the third-party-car-financial-claims body from claimCases + blameCases without calling Fanavaran.",
"Builds the GEN.03 third-party-car-financial-claims payload from local claimCases + blameCases without creating a Fanavaran claim.",
})
@ApiParam({
name: "client",
@@ -87,11 +87,11 @@ export class FanavaranController {
);
}
@Post(":client/claim-cases/:claimCaseId")
@Post(":client/claim-cases/:claimCaseId/base-claim/submit")
@ApiOperation({
summary: "Submit claim to Fanavaran (V2)",
summary: "Submit Fanavaran base claim create request",
description:
"Authenticates with the selected client credentials and submits the mapped claimCases + blameCases payload.",
"Authenticates with the selected tenant credentials and submits the GEN.03 base claim create request. Stores returned Id as claimId and ClaimNo when present.",
})
@ApiParam({
name: "client",
@@ -113,6 +113,162 @@ export class FanavaranController {
);
}
@Get(":client/claim-cases/:claimCaseId/damage-case/preview")
@ApiOperation({
summary: "Preview Fanavaran damage-case payload",
description:
"Builds the GEN.12 damaged vehicle/person case payload without calling Fanavaran. Requires selected damaged parts; submit requires a Fanavaran claimId.",
})
@ApiParam({
name: "client",
description: "Fanavaran tenant key",
enum: ["parsian", "tejaratno"],
})
@ApiParam({
name: "claimCaseId",
description: "Claim case MongoDB ObjectId",
})
async previewDamageCase(
@Param("client") client: string,
@Param("claimCaseId") claimCaseId: string,
) {
const clientKey = this.parseClientParam(client);
return await this.claimRequestManagementService.previewFanavaranDamageCaseV2(
claimCaseId,
clientKey,
);
}
@Post(":client/claim-cases/:claimCaseId/damage-case/submit")
@ApiOperation({
summary: "Submit Fanavaran damage-case request",
description:
"Submits the GEN.12 dmg-cases request for the already-created Fanavaran claim and stores returned Id as local dmgCaseId.",
})
@ApiParam({
name: "client",
description: "Fanavaran tenant key",
enum: ["parsian", "tejaratno"],
})
@ApiParam({
name: "claimCaseId",
description: "Claim case MongoDB ObjectId",
})
async submitDamageCase(
@Param("client") client: string,
@Param("claimCaseId") claimCaseId: string,
) {
const clientKey = this.parseClientParam(client);
return await this.claimRequestManagementService.submitFanavaranDamageCaseV2(
claimCaseId,
clientKey,
);
}
@Get(":client/claim-cases/:claimCaseId/attachments/preview")
@ApiOperation({
summary: "Preview Fanavaran attachment upload plan",
description:
"Lists local required-document and captured damage images that would be sent to GEN.07. Shows which images are already recorded as uploaded to Fanavaran.",
})
@ApiParam({
name: "client",
description: "Fanavaran tenant key",
enum: ["parsian", "tejaratno"],
})
@ApiParam({
name: "claimCaseId",
description: "Claim case MongoDB ObjectId",
})
async previewAttachments(
@Param("client") client: string,
@Param("claimCaseId") claimCaseId: string,
) {
const clientKey = this.parseClientParam(client);
return await this.claimRequestManagementService.previewFanavaranAttachmentsV2(
claimCaseId,
clientKey,
);
}
@Post(":client/claim-cases/:claimCaseId/attachments/submit")
@ApiOperation({
summary: "Submit missing Fanavaran attachments",
description:
"Uploads every local image that does not already have a recorded successful GEN.07 Fanavaran file upload. Uses one multipart request per image.",
})
@ApiParam({
name: "client",
description: "Fanavaran tenant key",
enum: ["parsian", "tejaratno"],
})
@ApiParam({
name: "claimCaseId",
description: "Claim case MongoDB ObjectId",
})
async submitAttachments(
@Param("client") client: string,
@Param("claimCaseId") claimCaseId: string,
) {
const clientKey = this.parseClientParam(client);
return await this.claimRequestManagementService.submitFanavaranAttachmentsV2(
claimCaseId,
clientKey,
);
}
@Get(":client/claim-cases/:claimCaseId/expertise/preview")
@ApiOperation({
summary: "Preview Fanavaran expertise payload",
description:
"Builds the GEN.08 expertise payload from the active damage expert reply, price-drop data, and Fanavaran lookup mappings without calling Fanavaran.",
})
@ApiParam({
name: "client",
description: "Fanavaran tenant key",
enum: ["parsian", "tejaratno"],
})
@ApiParam({
name: "claimCaseId",
description: "Claim case MongoDB ObjectId",
})
async previewExpertise(
@Param("client") client: string,
@Param("claimCaseId") claimCaseId: string,
) {
const clientKey = this.parseClientParam(client);
return await this.claimRequestManagementService.previewFanavaranExpertiseV2(
claimCaseId,
clientKey,
);
}
@Post(":client/claim-cases/:claimCaseId/expertise/submit")
@ApiOperation({
summary: "Submit Fanavaran expertise request",
description:
"Submits the GEN.08 expertise payload for a claim with existing Fanavaran claimId and dmgCaseId. Stores returned Id as local expertiseId.",
})
@ApiParam({
name: "client",
description: "Fanavaran tenant key",
enum: ["parsian", "tejaratno"],
})
@ApiParam({
name: "claimCaseId",
description: "Claim case MongoDB ObjectId",
})
async submitExpertise(
@Param("client") client: string,
@Param("claimCaseId") claimCaseId: string,
) {
const clientKey = this.parseClientParam(client);
return await this.claimRequestManagementService.submitFanavaranExpertiseV2(
claimCaseId,
clientKey,
);
}
private parseClientParam(client: string) {
if (!isFanavaranClientKey(client)) {
throw new BadRequestException(

View File

@@ -8,6 +8,9 @@ export enum FanavaranAuditStep {
POLICY_INQUIRY = "POLICY_INQUIRY",
BUILD_PAYLOAD = "BUILD_PAYLOAD",
SUBMIT_CLAIM = "SUBMIT_CLAIM",
SUBMIT_DAMAGE_CASE = "SUBMIT_DAMAGE_CASE",
SUBMIT_ATTACHMENT = "SUBMIT_ATTACHMENT",
SUBMIT_EXPERTISE = "SUBMIT_EXPERTISE",
}
export enum FanavaranAuditStatus {
@@ -31,13 +34,23 @@ export class FanavaranAuditLog {
@Prop({ type: String, required: true, enum: FanavaranAuditStep, index: true })
step: FanavaranAuditStep;
@Prop({ type: String, required: true, enum: FanavaranAuditStatus, index: true })
@Prop({
type: String,
required: true,
enum: FanavaranAuditStatus,
index: true,
})
status: FanavaranAuditStatus;
@Prop({ type: String, required: true, index: true })
clientKey: FanavaranClientKey;
@Prop({ type: String, required: true, enum: FanavaranAuditSource, index: true })
@Prop({
type: String,
required: true,
enum: FanavaranAuditSource,
index: true,
})
source: FanavaranAuditSource;
@Prop({ type: Types.ObjectId, required: false, index: true })

View File

@@ -1,5 +1,10 @@
import { Controller, Get } from "@nestjs/common";
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
import { Controller, Get, Param } from "@nestjs/common";
import {
ApiOkResponse,
ApiOperation,
ApiParam,
ApiTags,
} from "@nestjs/swagger";
import { LookupsService } from "./lookups.service";
@ApiTags("lookups")
@@ -96,9 +101,113 @@ export class LookupsController {
return await this.lookupsService.getAccidentCulpritType();
}
@Get("inspection-place")
@ApiOperation({
summary: "Fanavaran GEN.08 inspection place lookup",
description:
"Returns values for expertise payload field InspectionPlaceId from car/code-list/inspection-place.",
})
@ApiOkResponse({
description: "Returns Fanavaran inspection place lookup data",
schema: { type: "array", items: { type: "object" } },
})
async getInspectionPlace() {
return await this.lookupsService.getInspectionPlace();
}
@Get("drop-amount-status")
@ApiOperation({
summary: "Fanavaran GEN.08 drop amount status lookup",
description:
"Returns values for expertise payload field DropAmountStatus from car/code-list/drop-amount-status.",
})
@ApiOkResponse({
description: "Returns Fanavaran drop amount status lookup data",
schema: { type: "array", items: { type: "object" } },
})
async getDropAmountStatus() {
return await this.lookupsService.getDropAmountStatus();
}
@Get("car-components")
@ApiOperation({
summary: "Fanavaran GEN.08 damaged section lookup",
description:
"Returns values for DmgSections[].DmgSectionId from car/base-info/car-components.",
})
@ApiOkResponse({
description: "Returns Fanavaran car component lookup data",
schema: { type: "array", items: { type: "object" } },
})
async getCarComponents() {
return await this.lookupsService.getCarComponents();
}
@Get("accident-level")
@ApiOperation({
summary: "Fanavaran GEN.08 accident level lookup",
description:
"Returns values for DmgSections[].AccidentLevel from car/code-list/accident-level.",
})
@ApiOkResponse({
description: "Returns Fanavaran accident level lookup data",
schema: { type: "array", items: { type: "object" } },
})
async getAccidentLevel() {
return await this.lookupsService.getAccidentLevel();
}
@Get("expert-status")
@ApiOperation({
summary: "Fanavaran GEN.08 expert status lookup",
description:
"Returns values for expertise response field Status from car/code-list/expert-status.",
})
@ApiOkResponse({
description: "Returns Fanavaran expert status lookup data",
schema: { type: "array", items: { type: "object" } },
})
async getExpertStatus() {
return await this.lookupsService.getExpertStatus();
}
@Get("fanavaran")
@ApiOperation({
summary: "List configured Fanavaran remote lookups",
description:
"Returns the lookup names available through /lookups/fanavaran/{lookupName}.",
})
async listFanavaranRemoteLookups() {
return this.lookupsService.listFanavaranRemoteLookups().map((lookup) => ({
name: lookup.name,
cacheFile: lookup.cacheFile,
url: lookup.url,
}));
}
@Get("fanavaran/:lookupName")
@ApiOperation({
summary: "Fetch a Fanavaran remote lookup by name",
description:
"Generic cached Fanavaran lookup fetcher for configured lookup names, including GEN.08 expertise lookups.",
})
@ApiParam({
name: "lookupName",
description:
"Configured Fanavaran lookup name, for example inspection-place, drop-amount-status, car-components, accident-level, expert-status",
})
@ApiOkResponse({
description: "Returns cached or live Fanavaran lookup data",
schema: { type: "array", items: { type: "object" } },
})
async getFanavaranRemoteLookup(@Param("lookupName") lookupName: string) {
return await this.lookupsService.getClientRemoteLookup(lookupName);
}
@Get("accident-way")
@ApiOkResponse({
description: "Returns accident way options for the add-accident-fields step",
description:
"Returns accident way options for the add-accident-fields step",
schema: {
type: "array",
items: {
@@ -205,4 +314,3 @@ export class LookupsController {
return await this.lookupsService.getAccidentFields();
}
}

View File

@@ -2,6 +2,7 @@ import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { resolveFanavaranClientKey } from "src/core/config/fanavaran-client.config";
import {
FANAVARAN_REMOTE_LOOKUPS,
type FanavaranRemoteLookupDefinition,
TEJARAT_STATIC_ACCIDENT_FILES,
} from "src/fanavaran/fanavaran-lookup.config";
import { FanavaranLookupService } from "src/fanavaran/fanavaran-lookup.service";
@@ -31,10 +32,14 @@ export class LookupsService {
return resolveFanavaranClientKey();
}
listFanavaranRemoteLookups(): FanavaranRemoteLookupDefinition[] {
return FANAVARAN_REMOTE_LOOKUPS;
}
private findRemoteLookup(name: string) {
const lookup = FANAVARAN_REMOTE_LOOKUPS.find((item) => item.name === name);
if (!lookup) {
throw new Error(`Unknown Fanavaran remote lookup: ${name}`);
throw new NotFoundException(`Unknown Fanavaran remote lookup: ${name}`);
}
return lookup;
}
@@ -52,7 +57,7 @@ export class LookupsService {
return doc.response;
}
private async getClientRemoteLookup(lookupName: string): Promise<unknown> {
async getClientRemoteLookup(lookupName: string): Promise<unknown> {
const clientKey = this.activeClientKey();
const definition = this.findRemoteLookup(lookupName);
@@ -90,6 +95,26 @@ export class LookupsService {
return await this.getClientRemoteLookup("accident-culprit-type");
}
async getInspectionPlace(): Promise<any> {
return await this.getClientRemoteLookup("inspection-place");
}
async getDropAmountStatus(): Promise<any> {
return await this.getClientRemoteLookup("drop-amount-status");
}
async getCarComponents(): Promise<any> {
return await this.getClientRemoteLookup("car-components");
}
async getAccidentLevel(): Promise<any> {
return await this.getClientRemoteLookup("accident-level");
}
async getExpertStatus(): Promise<any> {
return await this.getClientRemoteLookup("expert-status");
}
async getAccidentWay(): Promise<{ id: number; label: string }[]> {
const clientKey = this.activeClientKey();
const fileName = TEJARAT_STATIC_ACCIDENT_FILES.accidentWay;
@@ -129,10 +154,9 @@ export class LookupsService {
if (clientKey === "parsian") {
const cacheFile = "accident-reason-options.json";
const cached =
await this.fanavaranLookupService.readCacheFile<
{ id: number; label: string; fanavaran: number }[]
>(clientKey, cacheFile);
const cached = await this.fanavaranLookupService.readCacheFile<
{ id: number; label: string; fanavaran: number }[]
>(clientKey, cacheFile);
if (cached) {
return cached;
}