Files
esg/src/config/configuration.ts
s.hajizadeh 45ef396466 feat: add Persian error translations and comprehensive test suite
Implement normalized error handling with Persian translations across all
exception types, replace legacy NestJS exceptions with AppException,
and add unit, integration, and smoke tests.

- Add error catalog with gateway-owned codes and Persian messages
- Introduce AppException wrapping normalized error envelopes
- Add translateError helper for automatic messageFa population
- Remove claims module and update provider error normalization
- Add unit tests for error contracts and helper functions
- Add integration tests for admin and inquiry endpoints
- Add smoke tests for real provider connectivity
- Add test support utilities (auth mocks, assertions, app factory)
2026-06-29 17:25:19 +03:30

218 lines
7.5 KiB
TypeScript

import { InquiryType } from '../common/enums/inquiry-type.enum';
import { ProviderName } from '../common/enums/provider-name.enum';
export type AuthMethod = 'AMITIS' | 'SOAP' | 'OAUTH2' | 'NONE';
export interface InquiryConfig {
url: string;
username: string;
password: string;
apiKey: string;
authMethod: AuthMethod;
}
export interface ProviderEnvConfig {
enabled: boolean;
timeout: number;
maxRetries: number;
inquiries: Partial<Record<InquiryType, InquiryConfig>>;
}
export interface AmitisAuthServiceConfig {
baseUrl: string;
loginPath: string;
refreshPath: string;
tokenTimeZone: string;
timeout: number;
enabled: boolean;
}
export interface TejaratNouConfig {
enabled: boolean;
timeout: number;
maxRetries: number;
authUrl: string;
clientId: string;
clientSecret: string;
username: string;
password: string;
inquiries: Partial<Record<InquiryType, InquiryConfig>>;
}
export interface InquiryRoutingConfig {
defaultProvider: ProviderName;
fallbackEnabled: boolean;
fallbackProviders: ProviderName[];
}
/**
* Central configuration factory — maps environment variables to typed config.
* Provider routing is deployment-specific and lives under inquiryRouting.
*/
export default () => ({
app: {
port: parseInt(process.env.PORT ?? '8085', 10),
nodeEnv: process.env.NODE_ENV ?? 'development',
},
mongodb: {
uri: process.env.MONGODB_URI ?? 'mongodb://localhost:27017/inquiry-gateway',
},
auth: {
apiKey: process.env.API_KEY ?? '',
jwtSecret: process.env.JWT_SECRET ?? 'change-me-in-production',
jwtRefreshSecret: process.env.JWT_REFRESH_SECRET ?? process.env.JWT_SECRET ?? 'change-me-refresh',
jwtAccessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN ?? '15m',
jwtRefreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? '7d',
jwtEnabled: process.env.JWT_ENABLED !== 'false',
bcryptSaltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS ?? '12', 10),
},
throttle: {
ttl: parseInt(process.env.THROTTLE_TTL ?? '60', 10),
limit: parseInt(process.env.THROTTLE_LIMIT ?? '100', 10),
},
amitis: {
baseUrl: process.env.AMITIS_BASE_URL ?? 'https://auth.services.centinsur.ir',
loginPath: process.env.AMITIS_LOGIN_PATH ?? '/api/security/login',
refreshPath: process.env.AMITIS_REFRESH_PATH ?? '/api/security/RefreshToken',
tokenTimeZone: process.env.AMITIS_TOKEN_TIME_ZONE ?? 'Asia/Tehran',
timeout: parseInt(process.env.AMITIS_TIMEOUT ?? '10000', 10),
enabled: process.env.AMITIS_ENABLED !== 'false',
} satisfies AmitisAuthServiceConfig,
hamta: buildProviderConfig('HAMTA'),
moallem: buildProviderConfig('MOALLEM'),
parsian: buildProviderConfig('PARSIAN'),
tejaratnou: {
enabled: process.env.TEJARATNOU_ENABLED !== 'false',
timeout: parseInt(process.env.TEJARATNOU_TIMEOUT ?? '15000', 10),
maxRetries: parseInt(process.env.TEJARATNOU_MAX_RETRIES ?? '2', 10),
authUrl: process.env.TEJARATNOU_AUTH_URL ?? 'https://accounts.tejaratnoins.ir',
clientId: process.env.TEJARATNOU_CLIENT_ID ?? 'api-gateway',
clientSecret: process.env.TEJARATNOU_CLIENT_SECRET ?? '',
username: process.env.TEJARATNOU_USERNAME ?? '',
password: process.env.TEJARATNOU_PASSWORD ?? '',
inquiries: {
[InquiryType.PERSON]: buildInquiryConfig('TEJARATNOU', 'PERSON'),
},
} satisfies TejaratNouConfig,
inquiryRouting: {
[InquiryType.PERSON]: buildInquiryRoutingConfig('PERSON', ProviderName.PARSIAN, [
ProviderName.HAMTA,
ProviderName.TEJARATNOU,
]),
[InquiryType.REAL_ESTATE]: buildInquiryRoutingConfig('REAL_ESTATE', ProviderName.HAMTA, [
ProviderName.MOALLEM,
]),
[InquiryType.SHEBA]: buildInquiryRoutingConfig('SHEBA', ProviderName.PARSIAN, [
ProviderName.HAMTA,
]),
[InquiryType.SHAHKAR]: buildInquiryRoutingConfig('SHAHKAR', ProviderName.PARSIAN, [
ProviderName.HAMTA,
]),
[InquiryType.POSTAL_CODE]: buildInquiryRoutingConfig('POSTAL_CODE', ProviderName.MOALLEM, [
ProviderName.HAMTA,
]),
[InquiryType.LEGAL_PERSON]: buildInquiryRoutingConfig('LEGAL_PERSON', ProviderName.HAMTA, [
ProviderName.MOALLEM,
]),
[InquiryType.CAR_PLATE]: buildInquiryRoutingConfig('CAR_PLATE', ProviderName.HAMTA, []),
[InquiryType.POLICY_BY_CHASSIS]: buildInquiryRoutingConfig(
'POLICY_BY_CHASSIS',
ProviderName.PARSIAN,
[],
),
[InquiryType.POLICY_BY_PLATE]: buildInquiryRoutingConfig(
'POLICY_BY_PLATE',
ProviderName.PARSIAN,
[],
),
[InquiryType.POLICY_BY_NATIONAL_CODE]: buildInquiryRoutingConfig(
'POLICY_BY_NATIONAL_CODE',
ProviderName.PARSIAN,
[],
),
} satisfies Record<InquiryType, InquiryRoutingConfig>,
});
function buildProviderConfig(prefix: string): ProviderEnvConfig {
const inquiryTypes = [
InquiryType.PERSON,
InquiryType.REAL_ESTATE,
InquiryType.SHAHKAR,
InquiryType.SHEBA,
InquiryType.POSTAL_CODE,
InquiryType.LEGAL_PERSON,
InquiryType.CAR_PLATE,
InquiryType.POLICY_BY_CHASSIS,
InquiryType.POLICY_BY_PLATE,
InquiryType.POLICY_BY_NATIONAL_CODE,
];
const inquiries: Partial<Record<InquiryType, InquiryConfig>> = {};
for (const inquiryType of inquiryTypes) {
const inquiryConfig = buildInquiryConfig(prefix, inquiryTypeToEnvName(inquiryType));
if (inquiryConfig.url || inquiryConfig.username) {
inquiries[inquiryType] = inquiryConfig;
}
}
return {
enabled: process.env[`${prefix}_ENABLED`] !== 'false',
timeout: parseInt(process.env[`${prefix}_TIMEOUT`] ?? '10000', 10),
maxRetries: parseInt(process.env[`${prefix}_MAX_RETRIES`] ?? '2', 10),
inquiries,
};
}
function buildInquiryConfig(providerPrefix: string, inquiryPrefix: string): InquiryConfig {
return {
url: process.env[`${providerPrefix}_${inquiryPrefix}_URL`] ?? '',
username: process.env[`${providerPrefix}_${inquiryPrefix}_USERNAME`] ?? '',
password: process.env[`${providerPrefix}_${inquiryPrefix}_PASSWORD`] ?? '',
apiKey: process.env[`${providerPrefix}_${inquiryPrefix}_API_KEY`] ?? '',
authMethod: (process.env[`${providerPrefix}_${inquiryPrefix}_AUTH_METHOD`] ?? 'NONE') as AuthMethod,
};
}
function inquiryTypeToEnvName(inquiryType: InquiryType): string {
const mapping: Record<InquiryType, string> = {
[InquiryType.PERSON]: 'PERSON',
[InquiryType.REAL_ESTATE]: 'REAL_ESTATE',
[InquiryType.SHAHKAR]: 'SHAHKAR',
[InquiryType.SHEBA]: 'SHEBA',
[InquiryType.POSTAL_CODE]: 'POSTAL_CODE',
[InquiryType.LEGAL_PERSON]: 'LEGAL_PERSON',
[InquiryType.CAR_PLATE]: 'CAR_PLATE',
[InquiryType.POLICY_BY_CHASSIS]: 'POLICY_BY_CHASSIS',
[InquiryType.POLICY_BY_PLATE]: 'POLICY_BY_PLATE',
[InquiryType.POLICY_BY_NATIONAL_CODE]: 'POLICY_BY_NATIONAL_CODE',
};
return mapping[inquiryType];
}
function buildInquiryRoutingConfig(
prefix: string,
defaultProvider: ProviderName,
fallbackProviders: ProviderName[],
): InquiryRoutingConfig {
return {
defaultProvider: (process.env[`${prefix}_DEFAULT_PROVIDER`] ?? defaultProvider) as ProviderName,
fallbackEnabled: parseBoolean(process.env[`${prefix}_FALLBACK_ENABLED`], false),
fallbackProviders: parseProviderList(
process.env[`${prefix}_FALLBACK_PROVIDERS`] ?? fallbackProviders.join(','),
),
};
}
function parseProviderList(value: string): ProviderName[] {
if (!value.trim()) return [];
return value.split(',').map((p) => p.trim() as ProviderName);
}
function parseBoolean(value: string | undefined, defaultValue: boolean): boolean {
if (value === undefined) return defaultValue;
return value.toLowerCase() === 'true';
}
// Made with Bob