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

@@ -0,0 +1,113 @@
import {
AUTH_MAX_ATTEMPTS,
buildAttemptSummary,
createAttemptTrail,
isAuthTransportRetryable,
isTransportRetryable,
recordResilienceAttempt,
shouldExposeAttemptSummary,
} from '../../src/common/helpers/provider-resilience.helper';
import { withRetry } from '../../src/common/helpers/retry.helper';
import { ProviderExecutionContext } from '../../src/common/interfaces/inquiry-provider.interface';
describe('provider resilience helpers', () => {
it('retries transport failures but not 429 or business rejects', () => {
expect(isTransportRetryable({ code: 'ETIMEDOUT' })).toBe(true);
expect(isTransportRetryable({ response: { status: 503 } })).toBe(true);
expect(isTransportRetryable({ response: { status: 429 } })).toBe(false);
expect(
isTransportRetryable({
normalizedError: { code: 'INQUIRY_NO_MATCH', message: 'no match' },
}),
).toBe(false);
});
it('auth retry is timeout/network only', () => {
expect(isAuthTransportRetryable({ code: 'ECONNRESET' })).toBe(true);
expect(isAuthTransportRetryable({ response: { status: 401 } })).toBe(false);
expect(isAuthTransportRetryable({ response: { status: 500 } })).toBe(false);
expect(AUTH_MAX_ATTEMPTS).toBe(2);
});
it('records every HTTP attempt and exposes summary on failure or retried success', () => {
const context: ProviderExecutionContext = {
requestId: 'req-1',
trackingCode: 'trk-1',
attemptTrail: createAttemptTrail(),
};
recordResilienceAttempt(context, 'HAMTA', {
attempt: 1,
durationMs: 100,
succeeded: false,
error: {
normalizedError: {
code: 'PROVIDER_TIMEOUT',
message: 'Provider request timed out',
messageFa: 'درخواست به سرویس‌دهنده زمان‌بر شد',
},
},
});
recordResilienceAttempt(context, 'HAMTA', {
attempt: 2,
durationMs: 80,
succeeded: true,
});
const summary = buildAttemptSummary(context.attemptTrail, 250);
expect(summary).toMatchObject({
totalAttempts: 2,
retried: true,
durationMs: 250,
});
expect(summary.attempts).toHaveLength(1);
expect(summary.attempts[0]).toMatchObject({
attempt: 1,
provider: 'HAMTA',
code: 'PROVIDER_TIMEOUT',
});
expect(shouldExposeAttemptSummary(true, summary)).toBe(true);
expect(
shouldExposeAttemptSummary(true, {
totalAttempts: 1,
retried: false,
durationMs: 10,
attempts: [],
}),
).toBe(false);
expect(
shouldExposeAttemptSummary(false, {
totalAttempts: 1,
retried: false,
durationMs: 10,
attempts: [],
}),
).toBe(true);
});
it('stops retrying when the deadline budget is exhausted', async () => {
let calls = 0;
const deadlineAt = Date.now() + 50;
await expect(
withRetry(
async () => {
calls += 1;
const error = new Error('timeout');
(error as NodeJS.ErrnoException).code = 'ETIMEDOUT';
throw error;
},
{
maxAttempts: 3,
delayMs: 40,
deadlineAt,
minRemainingMs: 100,
shouldRetry: isTransportRetryable,
},
),
).rejects.toMatchObject({ code: 'ETIMEDOUT' });
expect(calls).toBe(1);
});
});