forked from Shared/esg
Merge pull request 'main' (#7) from s.hajizadeh/esg:main into main
Reviewed-on: Shared/esg#7
This commit is contained in:
@@ -11,6 +11,7 @@ import { RateLimitModule } from './rate-limit/rate-limit.module';
|
|||||||
import { LoggingModule } from './logging/logging.module';
|
import { LoggingModule } from './logging/logging.module';
|
||||||
import { ProvidersModule } from './providers/providers.module';
|
import { ProvidersModule } from './providers/providers.module';
|
||||||
import { InquiryModule } from './inquiry/inquiry.module';
|
import { InquiryModule } from './inquiry/inquiry.module';
|
||||||
|
import { ClaimsModule } from './claims/claims.module';
|
||||||
import { DatabaseSeederModule } from './database/database-seeder.module';
|
import { DatabaseSeederModule } from './database/database-seeder.module';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,6 +35,7 @@ import { DatabaseSeederModule } from './database/database-seeder.module';
|
|||||||
DatabaseSeederModule,
|
DatabaseSeederModule,
|
||||||
ProvidersModule,
|
ProvidersModule,
|
||||||
InquiryModule,
|
InquiryModule,
|
||||||
|
ClaimsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule implements NestModule {
|
||||||
|
|||||||
32
src/claims/claims.controller.ts
Normal file
32
src/claims/claims.controller.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Body, Controller, Post, UseGuards, UseInterceptors } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Throttle } from '@nestjs/throttler';
|
||||||
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||||
|
import { ResponseTransformInterceptor } from '../common/interceptors/response-transform.interceptor';
|
||||||
|
import { UserRateLimitGuard } from '../rate-limit/user-rate-limit.guard';
|
||||||
|
import { SubmitThirdPartyClaimDto } from './dto/submit-third-party-claim.dto';
|
||||||
|
import { ThirdPartyClaimService, ThirdPartyClaimSubmitResult } from './third-party-claim.service';
|
||||||
|
|
||||||
|
@ApiTags('Claims')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('claims')
|
||||||
|
@UseGuards(JwtAuthGuard, UserRateLimitGuard)
|
||||||
|
@UseInterceptors(ResponseTransformInterceptor)
|
||||||
|
export class ClaimsController {
|
||||||
|
constructor(private readonly thirdPartyClaimService: ThirdPartyClaimService) {}
|
||||||
|
|
||||||
|
@Post('third-party/submit')
|
||||||
|
@Throttle({ default: { limit: 20, ttl: 60000 } })
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Submit a third-party car financial claim to Fanavaran',
|
||||||
|
description:
|
||||||
|
'Authenticates with Fanavaran, resolves the latest third-party policy for the national code, ' +
|
||||||
|
'resolves ClaimExpertId from evaluation history when provided, and submits the claim.',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 201, description: 'Claim submitted or structured failure returned' })
|
||||||
|
async submitThirdPartyClaim(
|
||||||
|
@Body() dto: SubmitThirdPartyClaimDto,
|
||||||
|
): Promise<ThirdPartyClaimSubmitResult> {
|
||||||
|
return this.thirdPartyClaimService.submit(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/claims/claims.module.ts
Normal file
31
src/claims/claims.module.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
import { ProvidersModule } from '../providers/providers.module';
|
||||||
|
import { ClaimsController } from './claims.controller';
|
||||||
|
import { FanavaranAuthService } from './fanavaran/fanavaran-auth.service';
|
||||||
|
import { FanavaranApiService } from './fanavaran/fanavaran-api.service';
|
||||||
|
import { EvaluationRequest, EvaluationRequestSchema } from './schemas/evaluation-request.schema';
|
||||||
|
import { FieldExpert, FieldExpertSchema } from './schemas/field-expert.schema';
|
||||||
|
import { ClaimExpertResolverService } from './services/claim-expert-resolver.service';
|
||||||
|
import { EvaluationRequestService } from './services/evaluation-request.service';
|
||||||
|
import { ThirdPartyClaimService } from './third-party-claim.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ProvidersModule,
|
||||||
|
MongooseModule.forFeature([
|
||||||
|
{ name: FieldExpert.name, schema: FieldExpertSchema },
|
||||||
|
{ name: EvaluationRequest.name, schema: EvaluationRequestSchema },
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
controllers: [ClaimsController],
|
||||||
|
providers: [
|
||||||
|
FanavaranAuthService,
|
||||||
|
FanavaranApiService,
|
||||||
|
EvaluationRequestService,
|
||||||
|
ClaimExpertResolverService,
|
||||||
|
ThirdPartyClaimService,
|
||||||
|
],
|
||||||
|
exports: [ThirdPartyClaimService, FanavaranApiService, ClaimExpertResolverService],
|
||||||
|
})
|
||||||
|
export class ClaimsModule {}
|
||||||
312
src/claims/dto/submit-third-party-claim.dto.ts
Normal file
312
src/claims/dto/submit-third-party-claim.dto.ts
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
IsEnum,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
Length,
|
||||||
|
Matches,
|
||||||
|
Max,
|
||||||
|
Min,
|
||||||
|
ValidateIf,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { FanavaranInsuranceLineId } from '../fanavaran/fanavaran.types';
|
||||||
|
|
||||||
|
export class SubmitThirdPartyClaimDto {
|
||||||
|
@ApiProperty({ example: '0059132574', description: 'Policy holder national code (10 digits)' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Length(10, 10)
|
||||||
|
@Matches(/^\d{10}$/, { message: 'nationalCode must be exactly 10 digits' })
|
||||||
|
nationalCode!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: FanavaranInsuranceLineId.THIRD_PARTY,
|
||||||
|
enum: FanavaranInsuranceLineId,
|
||||||
|
default: FanavaranInsuranceLineId.THIRD_PARTY,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(FanavaranInsuranceLineId)
|
||||||
|
@Type(() => Number)
|
||||||
|
insuranceLineId?: FanavaranInsuranceLineId;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Override ClaimExpertId instead of resolving from evaluation history',
|
||||||
|
example: 4662,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
claimExpertId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Evaluation/claim case id — latest expert from history is used when claimExpertId is omitted',
|
||||||
|
example: 'CASE-2026-001',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
evaluationRequestId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Use a specific policy id instead of resolving the latest policy from Fanavaran',
|
||||||
|
example: 14821240,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
policyId?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 701 })
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
AccidentCityId!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 155 })
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
AccidentReportTypeId!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1 })
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
AccidentVehicleUsedId!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 167 })
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
CompensationReferenceId!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 2 })
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
CulpritLicenceTypeId!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 337 })
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
CulpritTypeId!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 6 })
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
AccidentCauseId!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '1405/03/17' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
AccidentDate!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '1405/03/17' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
AnnouncementDate!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '1405/03/17' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
DocReceivedDate!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '15:59' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
AccidentTime!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'تهران، محله کوی نصر، بزرگراه جلال آل احمد' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
AccidentLocationAddress!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 143 })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
@Type(() => Number)
|
||||||
|
EstimateAmount!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((_obj, value) => value !== null)
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
AccidentCulpritId?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
CouponNo?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
CourtArchiveNo?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((_obj, value) => value !== null)
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
CulpritLicenceCityId?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((_obj, value) => value !== null)
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
CulpritLicenceCountryId?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
CulpritLicenceForeignCityName?: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '1394/10/13' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
CulpritLicenceIssuDate!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '1124242' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
CulpritLicenceNo!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1 })
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
DamagedCount!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
DmgAssessorFirstCreationTime?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
EntryDate?: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1 })
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Max(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
IsLicenseMatchWithVehicleKind!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 0 })
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Max(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
HasOtherCulprit!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 0 })
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Max(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
IsAccidentOutOfBorder!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 0 })
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Max(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
IsFatalAccident!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 0 })
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Max(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
IsPlaqueChanged!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((_obj, value) => value !== null)
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
PlaqueCityId?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((_obj, value) => value !== null)
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
PlaqueKindId?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((_obj, value) => value !== null)
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
PlaqueLeftNo?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((_obj, value) => value !== null)
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
PlaqueMiddleCodeId?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
PlaqueNo?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((_obj, value) => value !== null)
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
PlaqueRightNo?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((_obj, value) => value !== null)
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
PlaqueSampleId?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((_obj, value) => value !== null)
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
PlaqueSerial?: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 1 })
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
PoliceOfficerId!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
PoliceReportDesc?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
PoliceReportSeri?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
PoliceReportSerial?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '', default: '' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
PreviousPolicyEndDate?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
TrackingCode?: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 0 })
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Max(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
IsLicenseReplacement!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((_obj, value) => value !== null)
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
UnknownCulpritCauseId?: number | null;
|
||||||
|
}
|
||||||
119
src/claims/fanavaran/fanavaran-api.service.ts
Normal file
119
src/claims/fanavaran/fanavaran-api.service.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AxiosError, AxiosInstance } from 'axios';
|
||||||
|
import { createOutboundAxiosInstance } from '../../common/helpers/http-client.helper';
|
||||||
|
import { FanavaranConfig } from '../../config/configuration';
|
||||||
|
import { FanavaranAuthService } from './fanavaran-auth.service';
|
||||||
|
import {
|
||||||
|
FanavaranInsuranceLineId,
|
||||||
|
FanavaranPolicySummary,
|
||||||
|
ThirdPartyCarFinancialClaimPayload,
|
||||||
|
} from './fanavaran.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FanavaranApiService {
|
||||||
|
private readonly logger = new Logger(FanavaranApiService.name);
|
||||||
|
private readonly httpClient: AxiosInstance;
|
||||||
|
private readonly config: FanavaranConfig;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
configService: ConfigService,
|
||||||
|
private readonly authService: FanavaranAuthService,
|
||||||
|
) {
|
||||||
|
this.config = configService.get<FanavaranConfig>('fanavaran')!;
|
||||||
|
this.httpClient = createOutboundAxiosInstance({
|
||||||
|
baseURL: this.config.baseUrl,
|
||||||
|
timeout: this.config.timeout,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async inquiryLatestPolicy(
|
||||||
|
nationalCode: string,
|
||||||
|
insuranceLineId: FanavaranInsuranceLineId = FanavaranInsuranceLineId.THIRD_PARTY,
|
||||||
|
): Promise<FanavaranPolicySummary> {
|
||||||
|
const authenticationToken = await this.authService.getAuthenticationToken();
|
||||||
|
const response = await this.httpClient.get<FanavaranPolicySummary[]>(
|
||||||
|
'/api/BimeApi/v2.0/common/Policies/inquiry-my-policies',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
InsuranceLineId: insuranceLineId,
|
||||||
|
NationalCode: nationalCode,
|
||||||
|
},
|
||||||
|
headers: this.buildCommonHeaders(authenticationToken, this.config.policyInquiryLocation),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const policies = Array.isArray(response.data) ? response.data : [];
|
||||||
|
if (policies.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`No Fanavaran policies found for nationalCode=${nationalCode}, insuranceLineId=${insuranceLineId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const latest = this.pickLatestPolicy(policies);
|
||||||
|
this.logger.log(
|
||||||
|
`Fanavaran policy selected | nationalCode=${nationalCode} | policyId=${latest.PolicyId} | issuDate=${latest.IssuDate}`,
|
||||||
|
);
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitThirdPartyCarFinancialClaim(
|
||||||
|
payload: ThirdPartyCarFinancialClaimPayload,
|
||||||
|
): Promise<unknown> {
|
||||||
|
const authenticationToken = await this.authService.getAuthenticationToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.httpClient.post(
|
||||||
|
'/api/BimeApi/v2.0/car/third-party-car-financial-claims',
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
headers: this.buildCommonHeaders(authenticationToken, this.config.claimSubmitLocation),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw this.formatApiError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pickLatestPolicy(policies: FanavaranPolicySummary[]): FanavaranPolicySummary {
|
||||||
|
return [...policies].sort((left, right) => {
|
||||||
|
const dateCompare = right.IssuDate.localeCompare(left.IssuDate);
|
||||||
|
if (dateCompare !== 0) {
|
||||||
|
return dateCompare;
|
||||||
|
}
|
||||||
|
return right.PolicyId - left.PolicyId;
|
||||||
|
})[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCommonHeaders(
|
||||||
|
authenticationToken: string,
|
||||||
|
location: number,
|
||||||
|
): Record<string, string | number> {
|
||||||
|
return {
|
||||||
|
authenticationToken,
|
||||||
|
CorpId: this.config.corpId,
|
||||||
|
ContractId: this.config.contractId,
|
||||||
|
Location: location,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatApiError(error: unknown): Error {
|
||||||
|
if (!(error instanceof AxiosError)) {
|
||||||
|
return error instanceof Error ? error : new Error(String(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = error.response?.status;
|
||||||
|
const body =
|
||||||
|
typeof error.response?.data === 'string'
|
||||||
|
? error.response.data
|
||||||
|
: JSON.stringify(error.response?.data ?? {});
|
||||||
|
|
||||||
|
return new Error(
|
||||||
|
`Fanavaran API request failed${status ? ` (${status})` : ''}: ${error.message} | body=${body}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
141
src/claims/fanavaran/fanavaran-auth.service.ts
Normal file
141
src/claims/fanavaran/fanavaran-auth.service.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
import { createOutboundAxiosInstance } from '../../common/helpers/http-client.helper';
|
||||||
|
import { FanavaranConfig } from '../../config/configuration';
|
||||||
|
import { GeneralTokenService } from '../../providers/services/general-token.service';
|
||||||
|
import { FanavaranLoginUserInfo } from './fanavaran.types';
|
||||||
|
|
||||||
|
const APP_TOKEN_PROVIDER = 'Fanavaran:AppToken';
|
||||||
|
const AUTH_TOKEN_PROVIDER = 'Fanavaran:AuthToken';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FanavaranAuthService {
|
||||||
|
private readonly logger = new Logger(FanavaranAuthService.name);
|
||||||
|
private readonly httpClient: AxiosInstance;
|
||||||
|
private readonly config: FanavaranConfig;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
configService: ConfigService,
|
||||||
|
private readonly generalTokenService: GeneralTokenService,
|
||||||
|
) {
|
||||||
|
this.config = configService.get<FanavaranConfig>('fanavaran')!;
|
||||||
|
this.httpClient = createOutboundAxiosInstance({
|
||||||
|
baseURL: this.config.baseUrl,
|
||||||
|
timeout: this.config.timeout,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
this.config.enabled &&
|
||||||
|
Boolean(this.config.baseUrl) &&
|
||||||
|
Boolean(this.config.appName) &&
|
||||||
|
Boolean(this.config.appSecret) &&
|
||||||
|
Boolean(this.config.userName) &&
|
||||||
|
Boolean(this.config.password)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuthenticationToken(): Promise<string> {
|
||||||
|
const cached = await this.generalTokenService.getLatestToken(AUTH_TOKEN_PROVIDER);
|
||||||
|
if (cached && !(await this.generalTokenService.isTokenExpired(cached))) {
|
||||||
|
return cached.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appToken = await this.getAppToken();
|
||||||
|
const { authenticationToken } = await this.login(appToken);
|
||||||
|
return authenticationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAppToken(): Promise<string> {
|
||||||
|
const cached = await this.generalTokenService.getLatestToken(APP_TOKEN_PROVIDER);
|
||||||
|
if (cached && !(await this.generalTokenService.isTokenExpired(cached))) {
|
||||||
|
return cached.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.httpClient.post(
|
||||||
|
'/api/EITAuthentication/GetAppToken',
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
appname: this.config.appName,
|
||||||
|
secret: this.config.appSecret,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const appToken = this.readHeader(response.headers, 'apptoken');
|
||||||
|
if (!appToken) {
|
||||||
|
throw new Error('Fanavaran GetAppToken did not return appToken header');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.cacheToken(APP_TOKEN_PROVIDER, appToken, this.config.appTokenTtlSeconds);
|
||||||
|
return appToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async login(appToken: string): Promise<{
|
||||||
|
authenticationToken: string;
|
||||||
|
user: FanavaranLoginUserInfo;
|
||||||
|
}> {
|
||||||
|
const response = await this.httpClient.post('/api/EITAuthentication/Login', undefined, {
|
||||||
|
headers: {
|
||||||
|
appToken,
|
||||||
|
userName: this.config.userName,
|
||||||
|
password: this.config.password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const authenticationToken = this.readHeader(response.headers, 'authenticationtoken');
|
||||||
|
if (!authenticationToken) {
|
||||||
|
throw new Error('Fanavaran Login did not return authenticationToken header');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.cacheToken(
|
||||||
|
AUTH_TOKEN_PROVIDER,
|
||||||
|
authenticationToken,
|
||||||
|
this.config.authTokenTtlSeconds,
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = response.data as FanavaranLoginUserInfo;
|
||||||
|
this.logger.log(
|
||||||
|
`Fanavaran login succeeded | user=${user?.UserName ?? this.config.userName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { authenticationToken, user };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cacheToken(
|
||||||
|
serviceProvider: string,
|
||||||
|
token: string,
|
||||||
|
ttlSeconds: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
|
||||||
|
await this.generalTokenService.create({
|
||||||
|
serviceProvider,
|
||||||
|
tokenType: 'Bearer',
|
||||||
|
url: this.config.baseUrl,
|
||||||
|
clientId: this.config.appName,
|
||||||
|
clientSecret: '***',
|
||||||
|
username: this.config.userName,
|
||||||
|
scope: serviceProvider,
|
||||||
|
accessToken: token,
|
||||||
|
expiresIn: ttlSeconds,
|
||||||
|
expiresAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private readHeader(
|
||||||
|
headers: Record<string, unknown>,
|
||||||
|
name: string,
|
||||||
|
): string | undefined {
|
||||||
|
const value = headers[name];
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
if (Array.isArray(value) && typeof value[0] === 'string') {
|
||||||
|
return value[0].trim();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/claims/fanavaran/fanavaran.types.ts
Normal file
81
src/claims/fanavaran/fanavaran.types.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/** Fanavaran (Iran EIT) insurance line identifiers. */
|
||||||
|
export enum FanavaranInsuranceLineId {
|
||||||
|
CAR_BODY = 4,
|
||||||
|
THIRD_PARTY = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FanavaranPolicySummary {
|
||||||
|
BeginDate: string;
|
||||||
|
CustomerId: number;
|
||||||
|
EffectiveDate: string | null;
|
||||||
|
EndDate: string;
|
||||||
|
EndoNo: number;
|
||||||
|
InsuranceLineId: number;
|
||||||
|
IsClosed: number;
|
||||||
|
IssuDate: string;
|
||||||
|
OpUnitId: number;
|
||||||
|
PayerCustomerId: number;
|
||||||
|
PersonRoleId: number;
|
||||||
|
PolicyGroupId: number;
|
||||||
|
PolicyId: number;
|
||||||
|
PolicyNo: number;
|
||||||
|
PolicyVerNo: number;
|
||||||
|
Status: number;
|
||||||
|
TranTypeId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FanavaranLoginUserInfo {
|
||||||
|
UserName: string;
|
||||||
|
UserId: string;
|
||||||
|
FarsiName: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThirdPartyCarFinancialClaimPayload {
|
||||||
|
AccidentCityId: number;
|
||||||
|
AccidentReportTypeId: number;
|
||||||
|
AccidentVehicleUsedId: number;
|
||||||
|
ClaimExpertId: number;
|
||||||
|
CompensationReferenceId: number;
|
||||||
|
CulpritLicenceTypeId: number;
|
||||||
|
CulpritTypeId: number;
|
||||||
|
AccidentCauseId: number;
|
||||||
|
AccidentDate: string;
|
||||||
|
AnnouncementDate: string;
|
||||||
|
DocReceivedDate: string;
|
||||||
|
AccidentTime: string;
|
||||||
|
AccidentLocationAddress: string;
|
||||||
|
EstimateAmount: number;
|
||||||
|
AccidentCulpritId: number | null;
|
||||||
|
CouponNo: string | null;
|
||||||
|
CourtArchiveNo: string | null;
|
||||||
|
CulpritLicenceCityId: number | null;
|
||||||
|
CulpritLicenceCountryId: number | null;
|
||||||
|
CulpritLicenceForeignCityName: string | null;
|
||||||
|
CulpritLicenceIssuDate: string;
|
||||||
|
CulpritLicenceNo: string;
|
||||||
|
DamagedCount: number;
|
||||||
|
DmgAssessorFirstCreationTime: string | null;
|
||||||
|
EntryDate: string | null;
|
||||||
|
IsLicenseMatchWithVehicleKind: number;
|
||||||
|
HasOtherCulprit: number;
|
||||||
|
IsAccidentOutOfBorder: number;
|
||||||
|
IsFatalAccident: number;
|
||||||
|
IsPlaqueChanged: number;
|
||||||
|
PlaqueCityId: number | null;
|
||||||
|
PlaqueKindId: number | null;
|
||||||
|
PlaqueLeftNo: number | null;
|
||||||
|
PlaqueMiddleCodeId: number | null;
|
||||||
|
PlaqueNo: string | null;
|
||||||
|
PlaqueRightNo: number | null;
|
||||||
|
PlaqueSampleId: number | null;
|
||||||
|
PlaqueSerial: number | null;
|
||||||
|
PoliceOfficerId: number;
|
||||||
|
PoliceReportDesc: string | null;
|
||||||
|
PoliceReportSeri: string | null;
|
||||||
|
PoliceReportSerial: string | null;
|
||||||
|
PolicyId: number;
|
||||||
|
PreviousPolicyEndDate: string;
|
||||||
|
TrackingCode: string | null;
|
||||||
|
IsLicenseReplacement: number;
|
||||||
|
UnknownCulpritCauseId: number | null;
|
||||||
|
}
|
||||||
30
src/claims/schemas/evaluation-request.schema.ts
Normal file
30
src/claims/schemas/evaluation-request.schema.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||||
|
import { HydratedDocument, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export type EvaluationRequestDocument = HydratedDocument<EvaluationRequest>;
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class EvaluationHistoryEntry {
|
||||||
|
@Prop({ type: Types.ObjectId, ref: 'FieldExpert', required: true })
|
||||||
|
expertId!: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
expertCode?: number;
|
||||||
|
|
||||||
|
@Prop({ required: true, default: () => new Date() })
|
||||||
|
evaluatedAt!: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EvaluationHistoryEntrySchema =
|
||||||
|
SchemaFactory.createForClass(EvaluationHistoryEntry);
|
||||||
|
|
||||||
|
@Schema({ timestamps: true, collection: 'evaluation_requests' })
|
||||||
|
export class EvaluationRequest {
|
||||||
|
@Prop({ required: true, unique: true, trim: true })
|
||||||
|
requestId!: string;
|
||||||
|
|
||||||
|
@Prop({ type: [EvaluationHistoryEntrySchema], default: [] })
|
||||||
|
evaluations!: EvaluationHistoryEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EvaluationRequestSchema = SchemaFactory.createForClass(EvaluationRequest);
|
||||||
18
src/claims/schemas/field-expert.schema.ts
Normal file
18
src/claims/schemas/field-expert.schema.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||||
|
import { HydratedDocument, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export type FieldExpertDocument = HydratedDocument<FieldExpert>;
|
||||||
|
|
||||||
|
@Schema({ timestamps: true, collection: 'field_experts' })
|
||||||
|
export class FieldExpert {
|
||||||
|
@Prop({ required: true, trim: true })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Prop({ required: true, unique: true })
|
||||||
|
expertCode!: number;
|
||||||
|
|
||||||
|
@Prop({ default: true })
|
||||||
|
active!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FieldExpertSchema = SchemaFactory.createForClass(FieldExpert);
|
||||||
72
src/claims/services/claim-expert-resolver.service.ts
Normal file
72
src/claims/services/claim-expert-resolver.service.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { Model, Types } from 'mongoose';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { FanavaranConfig } from '../../config/configuration';
|
||||||
|
import { FieldExpert, FieldExpertDocument } from '../schemas/field-expert.schema';
|
||||||
|
import { EvaluationRequestService } from './evaluation-request.service';
|
||||||
|
|
||||||
|
export interface ClaimExpertResolutionInput {
|
||||||
|
claimExpertId?: number;
|
||||||
|
evaluationRequestId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ClaimExpertResolverService {
|
||||||
|
private readonly config: FanavaranConfig;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
configService: ConfigService,
|
||||||
|
@InjectModel(FieldExpert.name)
|
||||||
|
private readonly fieldExpertModel: Model<FieldExpertDocument>,
|
||||||
|
private readonly evaluationRequestService: EvaluationRequestService,
|
||||||
|
) {
|
||||||
|
this.config = configService.get<FanavaranConfig>('fanavaran')!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveClaimExpertId(input: ClaimExpertResolutionInput): Promise<number> {
|
||||||
|
if (input.claimExpertId !== undefined && input.claimExpertId !== null) {
|
||||||
|
return input.claimExpertId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.evaluationRequestId) {
|
||||||
|
const resolved = await this.resolveFromEvaluationHistory(input.evaluationRequestId);
|
||||||
|
if (resolved !== undefined) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.config.defaultClaimExpertId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveFromEvaluationHistory(
|
||||||
|
evaluationRequestId: string,
|
||||||
|
): Promise<number | undefined> {
|
||||||
|
const evaluationRequest =
|
||||||
|
await this.evaluationRequestService.findByRequestId(evaluationRequestId);
|
||||||
|
if (!evaluationRequest?.evaluations?.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestEvaluation = [...evaluationRequest.evaluations].sort(
|
||||||
|
(left, right) => right.evaluatedAt.getTime() - left.evaluatedAt.getTime(),
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
if (latestEvaluation.expertCode !== undefined && latestEvaluation.expertCode !== null) {
|
||||||
|
return latestEvaluation.expertCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!latestEvaluation.expertId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expert = await this.fieldExpertModel
|
||||||
|
.findOne({
|
||||||
|
_id: new Types.ObjectId(latestEvaluation.expertId),
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
return expert?.expertCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/claims/services/evaluation-request.service.ts
Normal file
19
src/claims/services/evaluation-request.service.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
import {
|
||||||
|
EvaluationRequest,
|
||||||
|
EvaluationRequestDocument,
|
||||||
|
} from '../schemas/evaluation-request.schema';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EvaluationRequestService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(EvaluationRequest.name)
|
||||||
|
private readonly evaluationRequestModel: Model<EvaluationRequestDocument>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findByRequestId(requestId: string): Promise<EvaluationRequestDocument | null> {
|
||||||
|
return this.evaluationRequestModel.findOne({ requestId }).exec();
|
||||||
|
}
|
||||||
|
}
|
||||||
159
src/claims/third-party-claim.service.ts
Normal file
159
src/claims/third-party-claim.service.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common';
|
||||||
|
import { generateTrackingCode } from '../common/helpers/tracking-code.helper';
|
||||||
|
import { SubmitThirdPartyClaimDto } from './dto/submit-third-party-claim.dto';
|
||||||
|
import { FanavaranApiService } from './fanavaran/fanavaran-api.service';
|
||||||
|
import { FanavaranAuthService } from './fanavaran/fanavaran-auth.service';
|
||||||
|
import {
|
||||||
|
FanavaranInsuranceLineId,
|
||||||
|
FanavaranPolicySummary,
|
||||||
|
ThirdPartyCarFinancialClaimPayload,
|
||||||
|
} from './fanavaran/fanavaran.types';
|
||||||
|
import { ClaimExpertResolverService } from './services/claim-expert-resolver.service';
|
||||||
|
|
||||||
|
export interface ThirdPartyClaimSubmitResult {
|
||||||
|
success: boolean;
|
||||||
|
trackingCode: string;
|
||||||
|
message: string;
|
||||||
|
duration: number;
|
||||||
|
data: {
|
||||||
|
policyId: number;
|
||||||
|
claimExpertId: number;
|
||||||
|
policy: FanavaranPolicySummary;
|
||||||
|
fanavaranResponse: unknown;
|
||||||
|
} | null;
|
||||||
|
error: { code: string; message: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ThirdPartyClaimService {
|
||||||
|
private readonly logger = new Logger(ThirdPartyClaimService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly fanavaranAuthService: FanavaranAuthService,
|
||||||
|
private readonly fanavaranApiService: FanavaranApiService,
|
||||||
|
private readonly claimExpertResolver: ClaimExpertResolverService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async submit(dto: SubmitThirdPartyClaimDto): Promise<ThirdPartyClaimSubmitResult> {
|
||||||
|
const trackingCode = generateTrackingCode();
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
if (!this.fanavaranAuthService.isEnabled()) {
|
||||||
|
throw new ServiceUnavailableException('Fanavaran integration is not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const insuranceLineId =
|
||||||
|
dto.insuranceLineId ?? FanavaranInsuranceLineId.THIRD_PARTY;
|
||||||
|
const claimExpertId = await this.claimExpertResolver.resolveClaimExpertId({
|
||||||
|
claimExpertId: dto.claimExpertId,
|
||||||
|
evaluationRequestId: dto.evaluationRequestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const policy =
|
||||||
|
dto.policyId !== undefined
|
||||||
|
? ({ PolicyId: dto.policyId } as FanavaranPolicySummary)
|
||||||
|
: await this.fanavaranApiService.inquiryLatestPolicy(
|
||||||
|
dto.nationalCode,
|
||||||
|
insuranceLineId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = this.buildClaimPayload(dto, policy.PolicyId, claimExpertId);
|
||||||
|
const fanavaranResponse =
|
||||||
|
await this.fanavaranApiService.submitThirdPartyCarFinancialClaim(payload);
|
||||||
|
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
this.logger.log(
|
||||||
|
`Third-party claim submitted | trackingCode=${trackingCode} | policyId=${policy.PolicyId} | claimExpertId=${claimExpertId} | durationMs=${duration}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
trackingCode,
|
||||||
|
message: 'Third-party claim submitted successfully',
|
||||||
|
duration,
|
||||||
|
data: {
|
||||||
|
policyId: policy.PolicyId,
|
||||||
|
claimExpertId,
|
||||||
|
policy,
|
||||||
|
fanavaranResponse,
|
||||||
|
},
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
const message = error instanceof Error ? error.message : 'Third-party claim submission failed';
|
||||||
|
this.logger.error(
|
||||||
|
`Third-party claim failed | trackingCode=${trackingCode} | durationMs=${duration} | ${message}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
trackingCode,
|
||||||
|
message,
|
||||||
|
duration,
|
||||||
|
data: null,
|
||||||
|
error: {
|
||||||
|
code: 'THIRD_PARTY_CLAIM_FAILED',
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildClaimPayload(
|
||||||
|
dto: SubmitThirdPartyClaimDto,
|
||||||
|
policyId: number,
|
||||||
|
claimExpertId: number,
|
||||||
|
): ThirdPartyCarFinancialClaimPayload {
|
||||||
|
return {
|
||||||
|
AccidentCityId: dto.AccidentCityId,
|
||||||
|
AccidentReportTypeId: dto.AccidentReportTypeId,
|
||||||
|
AccidentVehicleUsedId: dto.AccidentVehicleUsedId,
|
||||||
|
ClaimExpertId: claimExpertId,
|
||||||
|
CompensationReferenceId: dto.CompensationReferenceId,
|
||||||
|
CulpritLicenceTypeId: dto.CulpritLicenceTypeId,
|
||||||
|
CulpritTypeId: dto.CulpritTypeId,
|
||||||
|
AccidentCauseId: dto.AccidentCauseId,
|
||||||
|
AccidentDate: dto.AccidentDate,
|
||||||
|
AnnouncementDate: dto.AnnouncementDate,
|
||||||
|
DocReceivedDate: dto.DocReceivedDate,
|
||||||
|
AccidentTime: dto.AccidentTime,
|
||||||
|
AccidentLocationAddress: dto.AccidentLocationAddress,
|
||||||
|
EstimateAmount: dto.EstimateAmount,
|
||||||
|
AccidentCulpritId: dto.AccidentCulpritId ?? null,
|
||||||
|
CouponNo: dto.CouponNo ?? null,
|
||||||
|
CourtArchiveNo: dto.CourtArchiveNo ?? null,
|
||||||
|
CulpritLicenceCityId: dto.CulpritLicenceCityId ?? null,
|
||||||
|
CulpritLicenceCountryId: dto.CulpritLicenceCountryId ?? null,
|
||||||
|
CulpritLicenceForeignCityName: dto.CulpritLicenceForeignCityName ?? null,
|
||||||
|
CulpritLicenceIssuDate: dto.CulpritLicenceIssuDate,
|
||||||
|
CulpritLicenceNo: dto.CulpritLicenceNo,
|
||||||
|
DamagedCount: dto.DamagedCount,
|
||||||
|
DmgAssessorFirstCreationTime: dto.DmgAssessorFirstCreationTime ?? null,
|
||||||
|
EntryDate: dto.EntryDate ?? null,
|
||||||
|
IsLicenseMatchWithVehicleKind: dto.IsLicenseMatchWithVehicleKind,
|
||||||
|
HasOtherCulprit: dto.HasOtherCulprit,
|
||||||
|
IsAccidentOutOfBorder: dto.IsAccidentOutOfBorder,
|
||||||
|
IsFatalAccident: dto.IsFatalAccident,
|
||||||
|
IsPlaqueChanged: dto.IsPlaqueChanged,
|
||||||
|
PlaqueCityId: dto.PlaqueCityId ?? null,
|
||||||
|
PlaqueKindId: dto.PlaqueKindId ?? null,
|
||||||
|
PlaqueLeftNo: dto.PlaqueLeftNo ?? null,
|
||||||
|
PlaqueMiddleCodeId: dto.PlaqueMiddleCodeId ?? null,
|
||||||
|
PlaqueNo: dto.PlaqueNo ?? null,
|
||||||
|
PlaqueRightNo: dto.PlaqueRightNo ?? null,
|
||||||
|
PlaqueSampleId: dto.PlaqueSampleId ?? null,
|
||||||
|
PlaqueSerial: dto.PlaqueSerial ?? null,
|
||||||
|
PoliceOfficerId: dto.PoliceOfficerId,
|
||||||
|
PoliceReportDesc: dto.PoliceReportDesc ?? null,
|
||||||
|
PoliceReportSeri: dto.PoliceReportSeri ?? null,
|
||||||
|
PoliceReportSerial: dto.PoliceReportSerial ?? null,
|
||||||
|
PolicyId: policyId,
|
||||||
|
PreviousPolicyEndDate: dto.PreviousPolicyEndDate ?? '',
|
||||||
|
TrackingCode: dto.TrackingCode ?? null,
|
||||||
|
IsLicenseReplacement: dto.IsLicenseReplacement,
|
||||||
|
UnknownCulpritCauseId: dto.UnknownCulpritCauseId ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,4 +28,10 @@ export class NormalizedErrorDto {
|
|||||||
description: 'Field-level validation errors when code is VALIDATION_ERROR',
|
description: 'Field-level validation errors when code is VALIDATION_ERROR',
|
||||||
})
|
})
|
||||||
details?: Array<{ field: string; constraints: string[] }>;
|
details?: Array<{ field: string; constraints: string[] }>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: { nationalCode: '4311402422', NtnlId: '0015790231' },
|
||||||
|
description: 'Conflicting values when submitted data does not match provider result',
|
||||||
|
})
|
||||||
|
conflict?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,23 @@ export interface AmitisAuthServiceConfig {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FanavaranConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
baseUrl: string;
|
||||||
|
timeout: number;
|
||||||
|
appName: string;
|
||||||
|
appSecret: string;
|
||||||
|
userName: string;
|
||||||
|
password: string;
|
||||||
|
corpId: number;
|
||||||
|
contractId: number;
|
||||||
|
policyInquiryLocation: number;
|
||||||
|
claimSubmitLocation: number;
|
||||||
|
defaultClaimExpertId: number;
|
||||||
|
appTokenTtlSeconds: number;
|
||||||
|
authTokenTtlSeconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TejaratNouConfig {
|
export interface TejaratNouConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
timeout: number;
|
timeout: number;
|
||||||
@@ -81,6 +98,24 @@ export default () => ({
|
|||||||
hamta: buildProviderConfig('HAMTA'),
|
hamta: buildProviderConfig('HAMTA'),
|
||||||
moallem: buildProviderConfig('MOALLEM'),
|
moallem: buildProviderConfig('MOALLEM'),
|
||||||
parsian: buildProviderConfig('PARSIAN'),
|
parsian: buildProviderConfig('PARSIAN'),
|
||||||
|
fanavaran: {
|
||||||
|
enabled: process.env.FANAVARAN_ENABLED !== 'false',
|
||||||
|
baseUrl:
|
||||||
|
process.env.FANAVARAN_BASE_URL ??
|
||||||
|
'https://apimanager.iraneit.com/BimeApiManager',
|
||||||
|
timeout: parseInt(process.env.FANAVARAN_TIMEOUT ?? '30000', 10),
|
||||||
|
appName: process.env.FANAVARAN_APP_NAME ?? '',
|
||||||
|
appSecret: process.env.FANAVARAN_APP_SECRET ?? '',
|
||||||
|
userName: process.env.FANAVARAN_USERNAME ?? '',
|
||||||
|
password: process.env.FANAVARAN_PASSWORD ?? '',
|
||||||
|
corpId: parseInt(process.env.FANAVARAN_CORP_ID ?? '543', 10),
|
||||||
|
contractId: parseInt(process.env.FANAVARAN_CONTRACT_ID ?? '28', 10),
|
||||||
|
policyInquiryLocation: parseInt(process.env.FANAVARAN_POLICY_INQUIRY_LOCATION ?? '100100', 10),
|
||||||
|
claimSubmitLocation: parseInt(process.env.FANAVARAN_CLAIM_SUBMIT_LOCATION ?? '210050', 10),
|
||||||
|
defaultClaimExpertId: parseInt(process.env.FANAVARAN_DEFAULT_CLAIM_EXPERT_ID ?? '4662', 10),
|
||||||
|
appTokenTtlSeconds: parseInt(process.env.FANAVARAN_APP_TOKEN_TTL_SECONDS ?? '3600', 10),
|
||||||
|
authTokenTtlSeconds: parseInt(process.env.FANAVARAN_AUTH_TOKEN_TTL_SECONDS ?? '3600', 10),
|
||||||
|
} satisfies FanavaranConfig,
|
||||||
tejaratnou: {
|
tejaratnou: {
|
||||||
enabled: process.env.TEJARATNOU_ENABLED !== 'false',
|
enabled: process.env.TEJARATNOU_ENABLED !== 'false',
|
||||||
timeout: parseInt(process.env.TEJARATNOU_TIMEOUT ?? '15000', 10),
|
timeout: parseInt(process.env.TEJARATNOU_TIMEOUT ?? '15000', 10),
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { ApiProperty } from '@nestjs/swagger';
|
|||||||
import { IsNotEmpty, IsString, Matches } from 'class-validator';
|
import { IsNotEmpty, IsString, Matches } from 'class-validator';
|
||||||
|
|
||||||
export class PolicyByPlateRequestDto {
|
export class PolicyByPlateRequestDto {
|
||||||
|
@ApiProperty({ example: '4311402422', description: 'Owner national code (10 digits)' })
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^\d{10}$/, { message: 'nationalCode must be exactly 10 digits' })
|
||||||
|
nationalCode!: string;
|
||||||
|
|
||||||
@ApiProperty({ example: '12', description: 'Two digits on the left side of the plate' })
|
@ApiProperty({ example: '12', description: 'Two digits on the left side of the plate' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Matches(/^\d{2}$/, { message: 'plk1 must be exactly 2 digits' })
|
@Matches(/^\d{2}$/, { message: 'plk1 must be exactly 2 digits' })
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export abstract class BaseProvider<TRequest = unknown, TResponse = unknown>
|
|||||||
providerCode: partial.providerCode,
|
providerCode: partial.providerCode,
|
||||||
providerTrackingCode: partial.providerTrackingCode,
|
providerTrackingCode: partial.providerTrackingCode,
|
||||||
details: partial.details,
|
details: partial.details,
|
||||||
|
conflict: partial.conflict,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +136,7 @@ export abstract class BaseProvider<TRequest = unknown, TResponse = unknown>
|
|||||||
providerMessage?: string,
|
providerMessage?: string,
|
||||||
providerCode?: string,
|
providerCode?: string,
|
||||||
fallbackMessage = 'Provider request failed',
|
fallbackMessage = 'Provider request failed',
|
||||||
extras?: Pick<NormalizedErrorDto, 'providerTrackingCode' | 'details'>,
|
extras?: Pick<NormalizedErrorDto, 'providerTrackingCode' | 'details' | 'conflict'>,
|
||||||
): Error {
|
): Error {
|
||||||
const message = providerMessage?.trim() || fallbackMessage;
|
const message = providerMessage?.trim() || fallbackMessage;
|
||||||
const code = providerCode?.trim() || 'PROVIDER_ERROR';
|
const code = providerCode?.trim() || 'PROVIDER_ERROR';
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ interface ParsianPolicyByNationalCodePayload extends LegacyInquiryPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ParsianPolicyByPlatePayload extends LegacyInquiryPayload {
|
interface ParsianPolicyByPlatePayload extends LegacyInquiryPayload {
|
||||||
|
nationalCode?: string;
|
||||||
plk1?: string;
|
plk1?: string;
|
||||||
plk2?: string;
|
plk2?: string;
|
||||||
plk3?: string;
|
plk3?: string;
|
||||||
@@ -334,6 +335,7 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
|
|||||||
private async inquirePolicyByPlate(
|
private async inquirePolicyByPlate(
|
||||||
payload: ParsianPolicyByPlatePayload,
|
payload: ParsianPolicyByPlatePayload,
|
||||||
): Promise<{ raw: unknown }> {
|
): Promise<{ raw: unknown }> {
|
||||||
|
const nationalCode = this.getRequiredString(payload.nationalCode, 'nationalCode');
|
||||||
const plk1 = this.getRequiredString(payload.plk1, 'plk1');
|
const plk1 = this.getRequiredString(payload.plk1, 'plk1');
|
||||||
const plk2 = this.getRequiredString(payload.plk2, 'plk2');
|
const plk2 = this.getRequiredString(payload.plk2, 'plk2');
|
||||||
const plk3 = this.getRequiredString(payload.plk3, 'plk3');
|
const plk3 = this.getRequiredString(payload.plk3, 'plk3');
|
||||||
@@ -344,8 +346,24 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
|
|||||||
{ Plk1: plk1, Plk2: plk2, Plk3: plk3, PlkSrl: plksrl },
|
{ Plk1: plk1, Plk2: plk2, Plk3: plk3, PlkSrl: plksrl },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const policyNationalId = policy.NtnlId?.trim() ?? '';
|
||||||
|
if (!policyNationalId || !this.nationalIdsMatch(nationalCode, policyNationalId)) {
|
||||||
|
throw this.formatProviderError(
|
||||||
|
'عدم تطابق اطلاعات',
|
||||||
|
'INQUIRY_NO_MATCH',
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
conflict: {
|
||||||
|
nationalCode,
|
||||||
|
NtnlId: policyNationalId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
raw: {
|
raw: {
|
||||||
|
nationalCode,
|
||||||
plk1,
|
plk1,
|
||||||
plk2,
|
plk2,
|
||||||
plk3,
|
plk3,
|
||||||
@@ -355,6 +373,10 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private nationalIdsMatch(requested: string, fromPolicy: string): boolean {
|
||||||
|
return requested.trim().padStart(10, '0') === fromPolicy.trim().padStart(10, '0');
|
||||||
|
}
|
||||||
|
|
||||||
private async callCarPolicySoap(
|
private async callCarPolicySoap(
|
||||||
inquiryType: InquiryType,
|
inquiryType: InquiryType,
|
||||||
methodName: string,
|
methodName: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user