forked from Shared/esg
some error handler + error validation + realEstate bug fixed for hamta end moallem
This commit is contained in:
12
.env.example
12
.env.example
@@ -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
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -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,7 +131,7 @@ 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 |
|
||||||
|
|||||||
@@ -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:**
|
||||||
|
|||||||
@@ -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[] }>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
|
||||||
|
if (typeof res === 'object' && res !== null) {
|
||||||
|
if ('error' in res && isNormalizedError((res as { error: unknown }).error)) {
|
||||||
return (res as { error: NormalizedErrorDto }).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),
|
||||||
|
|||||||
97
src/common/helpers/validation-error.helper.ts
Normal file
97
src/common/helpers/validation-error.helper.ts
Normal 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'
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
]),
|
]),
|
||||||
|
|||||||
18
src/inquiry/dto/real-estate-request.dto.ts
Normal file
18
src/inquiry/dto/real-estate-request.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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')
|
||||||
|
|||||||
10
src/main.ts
10
src/main.ts
@@ -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,
|
||||||
|
});
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}>`),
|
||||||
|
|||||||
Reference in New Issue
Block a user