feat: add car inquiries with resilience, ownership privacy, and retry visibility

Introduce plate, chassis, and third-party car endpoints, a wall-clock inquiry
deadline with transport-only retries, and ownership-safe no-match errors so
another person's identity never leaks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 17:21:16 +03:30
parent b4be98b156
commit 61098f1bf4
40 changed files with 2595 additions and 374 deletions

View File

@@ -12,6 +12,12 @@ import {
AppErrorCode,
buildNormalizedError,
} from '../../common/constants/error-messages';
import {
AUTH_MAX_ATTEMPTS,
AUTH_RETRY_DELAY_MS,
isAuthTransportRetryable,
} from '../../common/helpers/provider-resilience.helper';
import { withRetry } from '../../common/helpers/retry.helper';
import { AmitisAuthServiceConfig } from '../../config/configuration';
import { GeneralTokenDocument } from '../schemas/general-token.schema';
import { GeneralTokenService } from '../services/general-token.service';
@@ -124,14 +130,16 @@ export class AmitisProvider {
`AMITIS login → POST ${loginUrl} | provider=${providerName} | inquiry=${inquiryType} | username=${username.toLowerCase()}`,
);
const response = await this.httpClient.post<CentInsurTokenResponse>(
this.config.loginPath,
loginBody.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
const response = await this.withGentleAuthRetry(() =>
this.httpClient.post<CentInsurTokenResponse>(
this.config.loginPath,
loginBody.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
},
),
);
if (isOutboundHttpDebugEnabled()) {
@@ -142,7 +150,7 @@ export class AmitisProvider {
const token = this.extractToken(response.data);
await this.saveToken(token, providerName, inquiryType, username, password, 'login');
await this.saveToken(token, providerName, inquiryType, username, 'login');
return token.accessToken;
} catch (error) {
throw this.toAuthError(`AMITIS login failed for ${providerName}/${inquiryType}`, error);
@@ -155,10 +163,12 @@ export class AmitisProvider {
inquiryType: InquiryType,
): Promise<string> {
try {
const response = await this.httpClient.post<CentInsurTokenResponse>(this.config.refreshPath, {
Token: latestToken.accessToken,
RefreshToken: latestToken.refreshToken,
});
const response = await this.withGentleAuthRetry(() =>
this.httpClient.post<CentInsurTokenResponse>(this.config.refreshPath, {
Token: latestToken.accessToken,
RefreshToken: latestToken.refreshToken,
}),
);
const token = this.extractToken(response.data, latestToken.refreshToken);
await this.saveToken(
@@ -166,7 +176,6 @@ export class AmitisProvider {
providerName,
inquiryType,
latestToken.username,
latestToken.clientSecret,
'refresh',
);
return token.accessToken;
@@ -178,20 +187,27 @@ export class AmitisProvider {
}
}
/** One gentle retry on timeout/network only — never on 401/403/429/auth rejects. */
private async withGentleAuthRetry<T>(fn: () => Promise<T>): Promise<T> {
return withRetry(fn, {
maxAttempts: AUTH_MAX_ATTEMPTS,
delayMs: AUTH_RETRY_DELAY_MS,
shouldRetry: isAuthTransportRetryable,
});
}
private async saveToken(
token: AmitisAuthToken,
providerName: ProviderName,
inquiryType: InquiryType,
username: string,
password: string,
scope: string,
): Promise<void> {
await this.generalTokenService.create({
await this.generalTokenService.persistToken({
serviceProvider: this.getTokenKey(providerName, inquiryType),
tokenType: token.tokenType,
url: this.config.baseUrl,
clientId: username.toLowerCase(),
clientSecret: password,
username: username.toLowerCase(),
scope,
accessToken: token.accessToken,