some error handler + error validation + realEstate bug fixed for hamta end moallem

This commit is contained in:
2026-06-14 12:16:48 +03:30
parent 2cb3340f40
commit b629a47f7f
14 changed files with 299 additions and 39 deletions

View File

@@ -19,9 +19,9 @@ THROTTLE_TTL=60
THROTTLE_LIMIT=100 THROTTLE_LIMIT=100
# Provider routing (per inquiry type) # Provider routing (per inquiry type)
PERSON_DEFAULT_PROVIDER=HAMTA PERSON_DEFAULT_PROVIDER=PARSIAN
PERSON_FALLBACK_ENABLED=true PERSON_FALLBACK_ENABLED=true
PERSON_FALLBACK_PROVIDERS=MOALLEM,TEJARATNOU PERSON_FALLBACK_PROVIDERS=HAMTA,TEJARATNOU
SHAHKAR_DEFAULT_PROVIDER=PARSIAN SHAHKAR_DEFAULT_PROVIDER=PARSIAN
SHAHKAR_FALLBACK_ENABLED=false SHAHKAR_FALLBACK_ENABLED=false
SHAHKAR_FALLBACK_PROVIDERS= SHAHKAR_FALLBACK_PROVIDERS=
@@ -49,6 +49,10 @@ HAMTA_SECRET_KEY=
HAMTA_TIMEOUT=10000 HAMTA_TIMEOUT=10000
HAMTA_ENABLED=true HAMTA_ENABLED=true
HAMTA_MAX_RETRIES=2 HAMTA_MAX_RETRIES=2
HAMTA_PERSON_URL=http://reinsure.centinsur.ir/SabtV3Out
HAMTA_PERSON_USERNAME=
HAMTA_PERSON_PASSWORD=
HAMTA_PERSON_AUTH_METHOD=SOAP
# Moallem provider (identical API structure to Hamta) # Moallem provider (identical API structure to Hamta)
MOALLEM_BASE_URL=https://api.moallem.example.com MOALLEM_BASE_URL=https://api.moallem.example.com
@@ -63,6 +67,10 @@ MOALLEM_MAX_RETRIES=2
PARSIAN_ENABLED=true PARSIAN_ENABLED=true
PARSIAN_TIMEOUT=10000 PARSIAN_TIMEOUT=10000
PARSIAN_MAX_RETRIES=2 PARSIAN_MAX_RETRIES=2
PARSIAN_PERSON_URL=http://reinsure.centinsur.ir/SabtV3Out
PARSIAN_PERSON_USERNAME=
PARSIAN_PERSON_PASSWORD=
PARSIAN_PERSON_AUTH_METHOD=SOAP
PARSIAN_SHAHKAR_URL=https://apigateway.parsianinsurance.com/shahkarinqOut PARSIAN_SHAHKAR_URL=https://apigateway.parsianinsurance.com/shahkarinqOut
PARSIAN_SHAHKAR_API_KEY= PARSIAN_SHAHKAR_API_KEY=
PARSIAN_SHAHKAR_AUTH_METHOD=NONE PARSIAN_SHAHKAR_AUTH_METHOD=NONE

View File

@@ -111,14 +111,14 @@ Per-inquiry configuration via environment variables:
```env ```env
# Default provider and fallback chain # Default provider and fallback chain
PERSON_DEFAULT_PROVIDER=HAMTA PERSON_DEFAULT_PROVIDER=PARSIAN
PERSON_FALLBACK_PROVIDERS=MOALLEM PERSON_FALLBACK_PROVIDERS=HAMTA,TEJARATNOU
# Per-inquiry credentials and authentication # Per-inquiry credentials and authentication
HAMTA_PERSON_URL=https://api.example.com/person PARSIAN_PERSON_URL=http://reinsure.centinsur.ir/SabtV3Out
HAMTA_PERSON_USERNAME=user123 PARSIAN_PERSON_USERNAME=user123
HAMTA_PERSON_PASSWORD=pass123 PARSIAN_PERSON_PASSWORD=pass123
HAMTA_PERSON_AUTH_METHOD=AMITIS PARSIAN_PERSON_AUTH_METHOD=SOAP
# AMITIS authentication service # AMITIS authentication service
AMITIS_LOGIN_URL=https://auth.services.centinsur.ir/api/security/login AMITIS_LOGIN_URL=https://auth.services.centinsur.ir/api/security/login
@@ -131,16 +131,16 @@ See [Environment Configuration Guide](docs/ENVIRONMENT_CONFIGURATION.md) for com
| Inquiry Type | Providers | Description | | Inquiry Type | Providers | Description |
|--------------|-----------|-------------| |--------------|-----------|-------------|
| `PERSON_INQUIRY` | HAMTA, MOALLEM | Person information by national code and birth date | | `PERSON_INQUIRY` | PARSIAN, HAMTA, TEJARATNOU, MOALLEM | Person information by national code and birth date |
| `REAL_ESTATE_INQUIRY` | HAMTA | Real estate ownership information | | `REAL_ESTATE_INQUIRY` | HAMTA | Real estate ownership information |
| `POSTAL_CODE_INQUIRY` | HAMTA, MOALLEM | Address validation by postal code | | `POSTAL_CODE_INQUIRY` | HAMTA, MOALLEM | Address validation by postal code |
| `SHAHKAR_INQUIRY` | PARSIAN, HAMTA, MOALLEM | Mobile number verification | | `SHAHKAR_INQUIRY` | PARSIAN, HAMTA, MOALLEM | Mobile number verification |
| `CIVIL_REGISTRATION_INQUIRY` | MOALLEM | Civil registration data | | `CIVIL_REGISTRATION_INQUIRY` | MOALLEM | Civil registration data |
| `SHEBA_INQUIRY` | PARSIAN, HAMTA, MOALLEM | Bank account (SHEBA) validation | | `SHEBA_INQUIRY` | PARSIAN, HAMTA, MOALLEM | Bank account (SHEBA) validation |
| `SAYAH_INQUIRY` | TEJARATNOU | Sayah system inquiry | | `SAYAH_INQUIRY` | TEJARATNOU | Sayah system inquiry |
| `POLICY_BY_CHASSIS_INQUIRY` | PARSIAN | Car policy history by chassis number | | `POLICY_BY_CHASSIS_INQUIRY` | PARSIAN | Car policy history by chassis number |
| `POLICY_BY_PLATE_INQUIRY` | PARSIAN | Car policy history by national plate | | `POLICY_BY_PLATE_INQUIRY` | PARSIAN | Car policy history by national plate |
| `POLICY_BY_NATIONAL_CODE_INQUIRY` | PARSIAN | Car policy history by national code | | `POLICY_BY_NATIONAL_CODE_INQUIRY` | PARSIAN | Car policy history by national code |
See [API Documentation](docs/API_DOCUMENTATION.md) for detailed request/response formats. See [API Documentation](docs/API_DOCUMENTATION.md) for detailed request/response formats.

View File

@@ -112,11 +112,11 @@ HAMTA_ENABLED=true
HAMTA_TIMEOUT=10000 HAMTA_TIMEOUT=10000
HAMTA_MAX_RETRIES=3 HAMTA_MAX_RETRIES=3
# Person Inquiry (AMITIS auth) # Person Inquiry (SOAP auth)
HAMTA_PERSON_URL=https://api.hamta.example.com/api/inquiry/person HAMTA_PERSON_URL=http://reinsure.centinsur.ir/SabtV3Out
HAMTA_PERSON_USERNAME=pa6476 HAMTA_PERSON_USERNAME=hamta.sabtahval
HAMTA_PERSON_PASSWORD=ciiws@sabt92 HAMTA_PERSON_PASSWORD=your-password
HAMTA_PERSON_AUTH_METHOD=AMITIS HAMTA_PERSON_AUTH_METHOD=SOAP
# Real Estate Inquiry (AMITIS auth) # Real Estate Inquiry (AMITIS auth)
HAMTA_REAL_ESTATE_URL=https://apigw.services.centinsur.ir/amlakeskanservice/amlakeskan/inquiry HAMTA_REAL_ESTATE_URL=https://apigw.services.centinsur.ir/amlakeskanservice/amlakeskan/inquiry
@@ -203,6 +203,12 @@ PARSIAN_ENABLED=true
PARSIAN_TIMEOUT=10000 PARSIAN_TIMEOUT=10000
PARSIAN_MAX_RETRIES=2 PARSIAN_MAX_RETRIES=2
# Person Inquiry (SOAP auth)
PARSIAN_PERSON_URL=http://reinsure.centinsur.ir/SabtV3Out
PARSIAN_PERSON_USERNAME=pa6476
PARSIAN_PERSON_PASSWORD=your-password
PARSIAN_PERSON_AUTH_METHOD=SOAP
# Shahkar Inquiry (X-PACKAGE-API-KEY header) # Shahkar Inquiry (X-PACKAGE-API-KEY header)
PARSIAN_SHAHKAR_URL=https://apigateway.parsianinsurance.com/shahkarinqOut PARSIAN_SHAHKAR_URL=https://apigateway.parsianinsurance.com/shahkarinqOut
PARSIAN_SHAHKAR_API_KEY=your-package-api-key PARSIAN_SHAHKAR_API_KEY=your-package-api-key
@@ -277,22 +283,22 @@ Routing determines which provider handles each inquiry type and whether to use f
#### Person Inquiry (Single Provider) #### Person Inquiry (Single Provider)
```env ```env
PERSON_DEFAULT_PROVIDER=TEJARATNOU PERSON_DEFAULT_PROVIDER=PARSIAN
PERSON_FALLBACK_ENABLED=false PERSON_FALLBACK_ENABLED=false
PERSON_FALLBACK_PROVIDERS= PERSON_FALLBACK_PROVIDERS=
``` ```
**Behavior**: Only TEJARATNOU is used. If it fails, request fails. **Behavior**: Only PARSIAN is used. If it fails, request fails.
#### Person Inquiry (With Fallback) #### Person Inquiry (With Fallback)
```env ```env
PERSON_DEFAULT_PROVIDER=TEJARATNOU PERSON_DEFAULT_PROVIDER=PARSIAN
PERSON_FALLBACK_ENABLED=true PERSON_FALLBACK_ENABLED=true
PERSON_FALLBACK_PROVIDERS=HAMTA,MOALLEM PERSON_FALLBACK_PROVIDERS=HAMTA,TEJARATNOU
``` ```
**Behavior**: **Behavior**:
1. Try TEJARATNOU first 1. Try PARSIAN first
2. If fails, try HAMTA 2. If fails, try HAMTA
3. If fails, try MOALLEM 3. If fails, try TEJARATNOU
4. If all fail, return error 4. If all fail, return error
#### Real Estate Inquiry #### Real Estate Inquiry
@@ -497,9 +503,9 @@ AMITIS_HAMTA_PERSON_PASSWORD=ciiws@sabt92
### New Format (Current) ### New Format (Current)
```env ```env
HAMTA_PERSON_USERNAME=pa6476 PARSIAN_PERSON_USERNAME=pa6476
HAMTA_PERSON_PASSWORD=ciiws@sabt92 PARSIAN_PERSON_PASSWORD=your-password
HAMTA_PERSON_AUTH_METHOD=AMITIS PARSIAN_PERSON_AUTH_METHOD=SOAP
``` ```
**Key Changes:** **Key Changes:**

View File

@@ -16,4 +16,10 @@ export class NormalizedErrorDto {
@ApiPropertyOptional({ example: '503' }) @ApiPropertyOptional({ example: '503' })
providerCode?: string; providerCode?: string;
@ApiPropertyOptional({
example: [{ field: 'sheba', constraints: ['sheba must start with IR and contain 24 digits'] }],
description: 'Field-level validation errors when code is VALIDATION_ERROR',
})
details?: Array<{ field: string; constraints: string[] }>;
} }

View File

@@ -10,6 +10,7 @@ import { Request, Response } from 'express';
import { BaseInquiryResponseDto } from '../dto/base-inquiry-response.dto'; import { BaseInquiryResponseDto } from '../dto/base-inquiry-response.dto';
import { NormalizedErrorDto } from '../dto/normalized-error.dto'; import { NormalizedErrorDto } from '../dto/normalized-error.dto';
import { ProviderException } from '../exceptions/provider.exception'; import { ProviderException } from '../exceptions/provider.exception';
import { isNormalizedError } from '../helpers/validation-error.helper';
/** /**
* Global exception filter — normalizes all errors into BaseInquiryResponseDto * Global exception filter — normalizes all errors into BaseInquiryResponseDto
@@ -22,7 +23,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost): void { catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp(); const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>(); const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request & { trackingCode?: string }>(); const request = ctx.getRequest<Request & { requestId?: string; trackingCode?: string }>();
const status = const status =
exception instanceof HttpException exception instanceof HttpException
@@ -31,9 +32,10 @@ export class AllExceptionsFilter implements ExceptionFilter {
const normalizedError = this.extractNormalizedError(exception); const normalizedError = this.extractNormalizedError(exception);
const trackingCode = request.trackingCode ?? 'UNKNOWN'; const trackingCode = request.trackingCode ?? 'UNKNOWN';
const requestId = request.requestId ?? (request.headers['x-request-id'] as string | undefined) ?? 'unknown';
this.logger.error( this.logger.error(
`requestId=${request.headers['x-request-id']} | trackingCode=${trackingCode} | ${normalizedError.message}`, `requestId=${requestId} | trackingCode=${trackingCode} | code=${normalizedError.code} | ${normalizedError.message}`,
exception instanceof Error ? exception.stack : undefined, exception instanceof Error ? exception.stack : undefined,
); );
@@ -56,13 +58,22 @@ export class AllExceptionsFilter implements ExceptionFilter {
if (exception instanceof HttpException) { if (exception instanceof HttpException) {
const res = exception.getResponse(); const res = exception.getResponse();
if (typeof res === 'object' && res !== null && 'error' in res) {
return (res as { error: NormalizedErrorDto }).error; if (typeof res === 'object' && res !== null) {
if ('error' in res && isNormalizedError((res as { error: unknown }).error)) {
return (res as { error: NormalizedErrorDto }).error;
}
if (isNormalizedError(res)) {
return res;
}
} }
const message = const message =
typeof res === 'string' typeof res === 'string'
? res ? res
: (res as { message?: string | string[] }).message; : (res as { message?: string | string[] }).message;
return { return {
code: 'HTTP_ERROR', code: 'HTTP_ERROR',
message: Array.isArray(message) ? message.join(', ') : String(message ?? exception.message), message: Array.isArray(message) ? message.join(', ') : String(message ?? exception.message),

View File

@@ -0,0 +1,97 @@
import { ValidationError } from 'class-validator';
import { NormalizedErrorDto } from '../dto/normalized-error.dto';
export interface ValidationFieldError {
field: string;
constraints: string[];
}
export function flattenValidationErrors(
errors: ValidationError[],
parentPath = '',
): ValidationFieldError[] {
const result: ValidationFieldError[] = [];
for (const error of errors) {
const field = parentPath ? `${parentPath}.${error.property}` : error.property;
if (error.constraints) {
result.push({
field,
constraints: Object.values(error.constraints),
});
}
if (error.children?.length) {
result.push(...flattenValidationErrors(error.children, field));
}
}
return result;
}
export function buildValidationErrorResponse(errors: ValidationError[]): {
normalizedError: NormalizedErrorDto;
details: ValidationFieldError[];
} {
const details = flattenValidationErrors(errors);
const summary =
details.length > 0
? details.map((d) => `${d.field}: ${d.constraints.join(', ')}`).join('; ')
: 'Request validation failed';
return {
normalizedError: {
code: 'VALIDATION_ERROR',
message: summary,
details,
},
details,
};
}
export function formatHttpExceptionMessage(exception: unknown): string {
if (!(exception && typeof exception === 'object' && 'getResponse' in exception)) {
return exception instanceof Error ? exception.message : String(exception);
}
const response = (exception as { getResponse: () => unknown }).getResponse();
if (typeof response === 'string') {
return response;
}
if (typeof response !== 'object' || response === null) {
return exception instanceof Error ? exception.message : 'Request failed';
}
const body = response as {
message?: string | string[];
error?: unknown;
};
if (isNormalizedError(body.error)) {
return body.error.message;
}
if (Array.isArray(body.message)) {
return body.message.join('; ');
}
if (typeof body.message === 'string' && body.message.length > 0) {
return body.message;
}
return exception instanceof Error ? exception.message : 'Request failed';
}
export function isNormalizedError(value: unknown): value is NormalizedErrorDto {
return (
typeof value === 'object' &&
value !== null &&
'code' in value &&
'message' in value &&
typeof (value as NormalizedErrorDto).code === 'string' &&
typeof (value as NormalizedErrorDto).message === 'string'
);
}

View File

@@ -6,6 +6,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { Observable, tap } from 'rxjs'; import { Observable, tap } from 'rxjs';
import { RequestLogger } from '../helpers/request-logger.helper'; import { RequestLogger } from '../helpers/request-logger.helper';
import { formatHttpExceptionMessage } from '../helpers/validation-error.helper';
/** /**
* Logs HTTP request duration at the controller boundary. * Logs HTTP request duration at the controller boundary.
@@ -31,9 +32,10 @@ export class LoggingInterceptor implements NestInterceptor {
); );
}, },
error: (err: unknown) => { error: (err: unknown) => {
const detail = formatHttpExceptionMessage(err);
this.requestLogger.logFailure( this.requestLogger.logFailure(
{ requestId: req.requestId ?? 'unknown', durationMs: Date.now() - start }, { requestId: req.requestId ?? 'unknown', durationMs: Date.now() - start },
`${req.method} ${req.url} failed`, `${req.method} ${req.url} failed: ${detail}`,
err, err,
); );
}, },

View File

@@ -95,7 +95,10 @@ export default () => ({
}, },
} satisfies TejaratNouConfig, } satisfies TejaratNouConfig,
inquiryRouting: { inquiryRouting: {
[InquiryType.PERSON]: buildInquiryRoutingConfig('PERSON', ProviderName.TEJARATNOU, []), [InquiryType.PERSON]: buildInquiryRoutingConfig('PERSON', ProviderName.PARSIAN, [
ProviderName.HAMTA,
ProviderName.TEJARATNOU,
]),
[InquiryType.REAL_ESTATE]: buildInquiryRoutingConfig('REAL_ESTATE', ProviderName.HAMTA, [ [InquiryType.REAL_ESTATE]: buildInquiryRoutingConfig('REAL_ESTATE', ProviderName.HAMTA, [
ProviderName.MOALLEM, ProviderName.MOALLEM,
]), ]),

View File

@@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, Length, Matches } from 'class-validator';
export class RealEstateRequestDto {
@ApiProperty({ example: '5098961130', description: 'Iranian national code (10 digits)' })
@IsString()
@IsNotEmpty()
@Length(10, 10)
@Matches(/^\d{10}$/, { message: 'nationalCode must be exactly 10 digits' })
nationalCode!: string;
@ApiProperty({ example: '1349689554', description: 'Postal code (10 digits)' })
@IsString()
@IsNotEmpty()
@Length(10, 10)
@Matches(/^\d{10}$/, { message: 'postalCode must be exactly 10 digits' })
postalCode!: string;
}

View File

@@ -28,6 +28,7 @@ import { GenericInquiryResponseDto } from './dto/generic-inquiry-response.dto';
import { PostalCodeRequestDto } from './dto/postal-code-request.dto'; import { PostalCodeRequestDto } from './dto/postal-code-request.dto';
import { ShahkarRequestDto } from './dto/shahkar-request.dto'; import { ShahkarRequestDto } from './dto/shahkar-request.dto';
import { SayahRequestDto } from './dto/sayah-request.dto'; import { SayahRequestDto } from './dto/sayah-request.dto';
import { RealEstateRequestDto } from './dto/real-estate-request.dto';
import { PolicyByChassisRequestDto } from './dto/policy-by-chassis-request.dto'; import { PolicyByChassisRequestDto } from './dto/policy-by-chassis-request.dto';
import { PolicyByNationalCodeRequestDto } from './dto/policy-by-national-code-request.dto'; import { PolicyByNationalCodeRequestDto } from './dto/policy-by-national-code-request.dto';
import { PolicyByPlateRequestDto } from './dto/policy-by-plate-request.dto'; import { PolicyByPlateRequestDto } from './dto/policy-by-plate-request.dto';
@@ -68,10 +69,14 @@ export class InquiryController {
@ApiOperation({ summary: 'Real estate inquiry' }) @ApiOperation({ summary: 'Real estate inquiry' })
@ApiResponse({ status: 200, type: GenericInquiryResponseDto }) @ApiResponse({ status: 200, type: GenericInquiryResponseDto })
async inquireRealEstate( async inquireRealEstate(
@Body() payload: Record<string, unknown>, @Body() payload: RealEstateRequestDto,
@Req() req: Request & { requestId?: string; trackingCode?: string }, @Req() req: Request & { requestId?: string; trackingCode?: string },
): Promise<BaseInquiryResponseDto<Record<string, unknown>>> { ): Promise<BaseInquiryResponseDto<Record<string, unknown>>> {
return this.inquireGeneric(InquiryType.REAL_ESTATE, payload, req); return this.inquireGeneric(
InquiryType.REAL_ESTATE,
payload as unknown as Record<string, unknown>,
req,
);
} }
@Post('sheba') @Post('sheba')

View File

@@ -1,5 +1,5 @@
import './telemetry'; import './telemetry';
import { Logger, ValidationPipe } from '@nestjs/common'; import { BadRequestException, Logger, ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
@@ -7,6 +7,7 @@ import { NextFunction, Request, Response } from 'express';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { API_KEY_HEADER } from './common/constants/app.constants'; import { API_KEY_HEADER } from './common/constants/app.constants';
import { OpenTelemetryNestLogger } from './otel-nest-logger'; import { OpenTelemetryNestLogger } from './otel-nest-logger';
import { buildValidationErrorResponse } from './common/helpers/validation-error.helper';
import { import {
recordHttpRequestEnd, recordHttpRequestEnd,
recordHttpRequestStart, recordHttpRequestStart,
@@ -46,6 +47,13 @@ async function bootstrap(): Promise<void> {
forbidNonWhitelisted: true, forbidNonWhitelisted: true,
transform: true, transform: true,
transformOptions: { enableImplicitConversion: true }, transformOptions: { enableImplicitConversion: true },
exceptionFactory: (errors) => {
const { normalizedError } = buildValidationErrorResponse(errors);
return new BadRequestException({
message: normalizedError.message,
error: normalizedError,
});
},
}), }),
); );

View File

@@ -51,6 +51,7 @@ export class HamtaProvider extends LegacyApiProvider {
InquiryType.SHEBA, InquiryType.SHEBA,
InquiryType.SHAHKAR, InquiryType.SHAHKAR,
InquiryType.POSTAL_CODE, InquiryType.POSTAL_CODE,
InquiryType.REAL_ESTATE,
]; ];
private readonly hamtaConfig: ProviderEnvConfig; private readonly hamtaConfig: ProviderEnvConfig;

View File

@@ -51,6 +51,7 @@ export class MoallemProvider extends LegacyApiProvider {
InquiryType.SHEBA, InquiryType.SHEBA,
InquiryType.SHAHKAR, InquiryType.SHAHKAR,
InquiryType.POSTAL_CODE, InquiryType.POSTAL_CODE,
InquiryType.REAL_ESTATE,
]; ];
private readonly moallemConfig: ProviderEnvConfig; private readonly moallemConfig: ProviderEnvConfig;

View File

@@ -10,8 +10,16 @@ import { AmitisProvider } from './amitis.provider';
import { import {
LegacyInquiryPayload, LegacyInquiryPayload,
LegacyInquiryResult, LegacyInquiryResult,
PersonInquiryResult,
} from '../shared/legacy-api.provider.abstract'; } from '../shared/legacy-api.provider.abstract';
interface ParsianCivilRegistrationPayload extends LegacyInquiryPayload {
nationalCode?: string;
birthDate?: string;
NIN?: string;
BirthDate?: string;
}
interface ParsianShahkarPayload extends LegacyInquiryPayload { interface ParsianShahkarPayload extends LegacyInquiryPayload {
nationalCode?: string; nationalCode?: string;
nationalCod?: string; nationalCod?: string;
@@ -71,6 +79,7 @@ interface ParsianPolicyByPlatePayload extends LegacyInquiryPayload {
export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyInquiryResult> { export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyInquiryResult> {
readonly name = ProviderName.PARSIAN; readonly name = ProviderName.PARSIAN;
readonly supportedInquiryTypes = [ readonly supportedInquiryTypes = [
InquiryType.PERSON,
InquiryType.SHAHKAR, InquiryType.SHAHKAR,
InquiryType.SHEBA, InquiryType.SHEBA,
InquiryType.POLICY_BY_CHASSIS, InquiryType.POLICY_BY_CHASSIS,
@@ -86,6 +95,7 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
} }
isEnabled(): boolean { isEnabled(): boolean {
const personConfig = this.config.inquiries[InquiryType.PERSON];
const shahkarConfig = this.config.inquiries[InquiryType.SHAHKAR]; const shahkarConfig = this.config.inquiries[InquiryType.SHAHKAR];
const shebaConfig = this.config.inquiries[InquiryType.SHEBA]; const shebaConfig = this.config.inquiries[InquiryType.SHEBA];
const policyByChassisConfig = this.config.inquiries[InquiryType.POLICY_BY_CHASSIS]; const policyByChassisConfig = this.config.inquiries[InquiryType.POLICY_BY_CHASSIS];
@@ -95,7 +105,8 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
return ( return (
this.config.enabled && this.config.enabled &&
((Boolean(shahkarConfig?.url) && Boolean(shahkarConfig?.apiKey)) || (this.hasSoapCredentials(personConfig) ||
(Boolean(shahkarConfig?.url) && Boolean(shahkarConfig?.apiKey)) ||
(Boolean(shebaConfig?.url) && (Boolean(shebaConfig?.url) &&
Boolean(shebaConfig?.username) && Boolean(shebaConfig?.username) &&
Boolean(shebaConfig?.password)) || Boolean(shebaConfig?.password)) ||
@@ -110,6 +121,10 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
payload: LegacyInquiryPayload, payload: LegacyInquiryPayload,
_context: ProviderExecutionContext, _context: ProviderExecutionContext,
): Promise<LegacyInquiryResult> { ): Promise<LegacyInquiryResult> {
if (inquiryType === InquiryType.PERSON) {
return this.inquireCivilRegistration(payload as ParsianCivilRegistrationPayload);
}
if (inquiryType === InquiryType.SHAHKAR) { if (inquiryType === InquiryType.SHAHKAR) {
return this.inquireShahkar(payload as ParsianShahkarPayload); return this.inquireShahkar(payload as ParsianShahkarPayload);
} }
@@ -188,6 +203,64 @@ export class ParsianProvider extends BaseProvider<LegacyInquiryPayload, LegacyIn
} }
} }
private async inquireCivilRegistration(
payload: ParsianCivilRegistrationPayload,
): Promise<PersonInquiryResult> {
const nationalCode = this.getRequiredString(
payload.nationalCode ?? payload.NIN,
'nationalCode',
);
const birthDate = this.getRequiredString(payload.birthDate ?? payload.BirthDate, 'birthDate');
const inquiryConfig = this.config.inquiries[InquiryType.PERSON];
if (!this.hasSoapCredentials(inquiryConfig)) {
throw this.formatProviderError(
undefined,
undefined,
'Parsian person inquiry is not configured',
);
}
try {
const response = await axios.post<string>(
inquiryConfig.url,
this.buildCivilRegistrationEnvelope(
nationalCode,
birthDate,
inquiryConfig.username,
inquiryConfig.password,
),
{
headers: {
'Content-Type': 'text/xml; charset=utf-8',
SOAPAction: '"http://tempuri.org/ISabtInq/SubmitInqDteStsWithPstCod"',
},
timeout: this.config.timeout,
responseType: 'text',
},
);
return {
nationalCode,
birthDate,
raw: {
nationalCode,
birthDate,
result: this.extractSoapValue(response.data, 'SubmitInqDteStsWithPstCodResult'),
soap: response.data,
},
};
} catch (error) {
if (error instanceof AxiosError) {
throw this.formatProviderError(
error.message,
String(error.response?.status ?? 'NETWORK_ERROR'),
);
}
throw error;
}
}
private async inquirePolicyByChassis( private async inquirePolicyByChassis(
payload: ParsianPolicyByChassisPayload, payload: ParsianPolicyByChassisPayload,
): Promise<{ raw: unknown }> { ): Promise<{ raw: unknown }> {
@@ -401,6 +474,27 @@ ${fieldXml}
</soap:Envelope>`; </soap:Envelope>`;
} }
private buildCivilRegistrationEnvelope(
nationalCode: string,
birthDate: string,
username: string,
password: string,
): string {
return `<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<SubmitInqDteStsWithPstCod xmlns="http://tempuri.org/">
<NIN>${this.escapeXml(nationalCode)}</NIN>
<BirthDate>${this.escapeXml(birthDate)}</BirthDate>
<Username>${this.escapeXml(username)}</Username>
<Password>${this.escapeXml(password)}</Password>
</SubmitInqDteStsWithPstCod>
</soap:Body>
</soap:Envelope>`;
}
private extractSoapValue(xml: string, tagName: string): string | null { private extractSoapValue(xml: string, tagName: string): string | null {
const match = xml.match( const match = xml.match(
new RegExp(`<(?:\\w+:)?${tagName}[^>]*>([\\s\\S]*?)</(?:\\w+:)?${tagName}>`), new RegExp(`<(?:\\w+:)?${tagName}[^>]*>([\\s\\S]*?)</(?:\\w+:)?${tagName}>`),