fanavaran duplication request problems fixed.

This commit is contained in:
2026-08-02 17:00:19 +03:30
parent c2f5c576fa
commit b345818d43
9 changed files with 884 additions and 123 deletions

View File

@@ -6,7 +6,9 @@ import {
Logger,
ServiceUnavailableException,
} from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { isAxiosError } from "axios";
import { Model } from "mongoose";
import { firstValueFrom } from "rxjs";
import {
getFanavaranClientProfile,
@@ -19,6 +21,10 @@ import {
FanavaranAuditStatus,
FanavaranAuditStep,
} from "./schema/fanavaran-audit-log.schema";
import {
FanavaranAuthToken,
FanavaranAuthTokenDocument,
} from "./schema/fanavaran-auth-token.schema";
interface CachedFanavaranAuth {
authenticationToken: string;
@@ -63,6 +69,8 @@ export class FanavaranAuthService {
constructor(
private readonly httpService: HttpService,
private readonly fanavaranAuditService: FanavaranAuditService,
@InjectModel(FanavaranAuthToken.name)
private readonly authTokenModel: Model<FanavaranAuthTokenDocument>,
) {}
/** True when Fanavaran asked us to wait (Persian “try again later” / tracking-code 500). */
@@ -162,6 +170,12 @@ export class FanavaranAuthService {
invalidateToken(clientKey: FanavaranClientKey): void {
this.tokenCache.delete(clientKey);
void this.authTokenModel.deleteOne({ clientKey }).exec().catch((error) => {
this.logger.warn(
`[${clientKey}] Failed to clear persisted Fanavaran auth token`,
error,
);
});
}
assertNotInBackoff(clientKey: FanavaranClientKey): void {
@@ -183,9 +197,13 @@ export class FanavaranAuthService {
this.assertNotInBackoff(clientKey);
if (!options?.forceRefresh) {
const cached = this.tokenCache.get(clientKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.authenticationToken;
const memoryHit = this.readMemoryCache(clientKey);
if (memoryHit) {
return memoryHit;
}
const persisted = await this.readPersistedCache(clientKey);
if (persisted) {
return persisted;
}
} else {
this.invalidateToken(clientKey);
@@ -243,10 +261,7 @@ export class FanavaranAuthService {
);
const expiresAt = FanavaranAuthService.getNextMidnightExpiryMs();
this.tokenCache.set(clientKey, {
authenticationToken,
expiresAt,
});
await this.persistToken(clientKey, authenticationToken, expiresAt);
this.clearBackoff(clientKey);
this.logger.log(
`[${clientKey}] Cached Fanavaran authenticationToken until ${new Date(
@@ -256,6 +271,76 @@ export class FanavaranAuthService {
return authenticationToken;
}
private readMemoryCache(clientKey: FanavaranClientKey): string | null {
const cached = this.tokenCache.get(clientKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.authenticationToken;
}
if (cached) {
this.tokenCache.delete(clientKey);
}
return null;
}
private async readPersistedCache(
clientKey: FanavaranClientKey,
): Promise<string | null> {
try {
const doc = await this.authTokenModel.findOne({ clientKey }).lean().exec();
if (!doc?.authenticationToken || !doc.expiresAt) {
return null;
}
const expiresAt = new Date(doc.expiresAt).getTime();
if (!(expiresAt > Date.now())) {
await this.authTokenModel.deleteOne({ clientKey }).exec();
return null;
}
this.tokenCache.set(clientKey, {
authenticationToken: doc.authenticationToken,
expiresAt,
});
this.logger.log(
`[${clientKey}] Reused persisted Fanavaran authenticationToken until ${new Date(
expiresAt,
).toISOString()}`,
);
return doc.authenticationToken;
} catch (error) {
this.logger.warn(
`[${clientKey}] Failed to read persisted Fanavaran auth token`,
error,
);
return null;
}
}
private async persistToken(
clientKey: FanavaranClientKey,
authenticationToken: string,
expiresAt: number,
): Promise<void> {
this.tokenCache.set(clientKey, { authenticationToken, expiresAt });
try {
await this.authTokenModel
.findOneAndUpdate(
{ clientKey },
{
$set: {
authenticationToken,
expiresAt: new Date(expiresAt),
},
},
{ upsert: true, new: true },
)
.exec();
} catch (error) {
this.logger.warn(
`[${clientKey}] Failed to persist Fanavaran auth token (memory cache still active)`,
error,
);
}
}
private emptyBodyTransformRequest() {
return [
(_data: unknown, headers?: Record<string, unknown>) => {
@@ -273,12 +358,20 @@ export class FanavaranAuthService {
auditSession?: FanavaranAuditSession,
): Promise<string> {
const startedAt = Date.now();
const requestHeaders = {
appname: config.appName,
secret: config.secret,
"Content-Length": "0",
};
if (auditSession) {
await this.fanavaranAuditService.recordStep({
session: auditSession,
step: FanavaranAuditStep.GET_APP_TOKEN,
status: FanavaranAuditStatus.STARTED,
requestUrl: this.getAppTokenUrl,
requestMethod: "POST",
requestHeaders,
requestBody: "",
requestMeta: { appName: config.appName, cached: false },
});
}
@@ -286,11 +379,7 @@ export class FanavaranAuthService {
try {
const response = await firstValueFrom(
this.httpService.post(this.getAppTokenUrl, "", {
headers: {
appname: config.appName,
secret: config.secret,
"Content-Length": "0",
},
headers: requestHeaders,
transformRequest: this.emptyBodyTransformRequest(),
}),
);
@@ -306,13 +395,23 @@ export class FanavaranAuthService {
}
if (auditSession) {
const exchange =
this.fanavaranAuditService.captureAxiosExchange(
response,
requestHeaders,
);
await this.fanavaranAuditService.recordStep({
session: auditSession,
step: FanavaranAuditStep.GET_APP_TOKEN,
status: FanavaranAuditStatus.SUCCESS,
requestUrl: this.getAppTokenUrl,
requestMethod: "POST",
httpStatus: response.status,
responseMeta: { hasAppToken: true },
requestHeaders: exchange.requestHeaders,
requestBody: "",
responseHeaders: exchange.responseHeaders,
responseBody: exchange.responseBody,
responseMeta: { hasAppToken: true, cached: false },
durationMs: Date.now() - startedAt,
});
}
@@ -320,12 +419,21 @@ export class FanavaranAuthService {
return appToken;
} catch (error) {
if (auditSession) {
const exchange = this.fanavaranAuditService.captureAxiosExchange(
error,
requestHeaders,
);
await this.fanavaranAuditService.recordStep({
session: auditSession,
step: FanavaranAuditStep.GET_APP_TOKEN,
status: FanavaranAuditStatus.FAILURE,
requestUrl: this.getAppTokenUrl,
httpStatus: isAxiosError(error) ? error.response?.status : undefined,
requestMethod: "POST",
httpStatus: exchange.httpStatus,
requestHeaders: exchange.requestHeaders,
requestBody: "",
responseHeaders: exchange.responseHeaders,
responseBody: exchange.responseBody,
errorMessage: this.fanavaranAuditService.extractErrorMessage(error),
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
durationMs: Date.now() - startedAt,
@@ -341,12 +449,21 @@ export class FanavaranAuthService {
auditSession?: FanavaranAuditSession,
): Promise<string> {
const startedAt = Date.now();
const requestHeaders = {
appToken,
userName: config.username,
password: config.password,
"Content-Length": "0",
};
if (auditSession) {
await this.fanavaranAuditService.recordStep({
session: auditSession,
step: FanavaranAuditStep.LOGIN,
status: FanavaranAuditStatus.STARTED,
requestUrl: this.loginUrl,
requestMethod: "POST",
requestHeaders,
requestBody: "",
requestMeta: { userName: config.username, cached: false },
});
}
@@ -354,12 +471,7 @@ export class FanavaranAuthService {
try {
const response = await firstValueFrom(
this.httpService.post(this.loginUrl, "", {
headers: {
appToken,
userName: config.username,
password: config.password,
"Content-Length": "0",
},
headers: requestHeaders,
transformRequest: this.emptyBodyTransformRequest(),
}),
);
@@ -380,13 +492,26 @@ export class FanavaranAuthService {
}
if (auditSession) {
const exchange =
this.fanavaranAuditService.captureAxiosExchange(
response,
requestHeaders,
);
await this.fanavaranAuditService.recordStep({
session: auditSession,
step: FanavaranAuditStep.LOGIN,
status: FanavaranAuditStatus.SUCCESS,
requestUrl: this.loginUrl,
requestMethod: "POST",
httpStatus: response.status,
responseMeta: { hasAuthenticationToken: true },
requestHeaders: exchange.requestHeaders,
requestBody: "",
responseHeaders: exchange.responseHeaders,
responseBody: exchange.responseBody,
responseMeta: {
hasAuthenticationToken: true,
cached: false,
},
durationMs: Date.now() - startedAt,
});
}
@@ -394,12 +519,21 @@ export class FanavaranAuthService {
return authenticationToken;
} catch (error) {
if (auditSession) {
const exchange = this.fanavaranAuditService.captureAxiosExchange(
error,
requestHeaders,
);
await this.fanavaranAuditService.recordStep({
session: auditSession,
step: FanavaranAuditStep.LOGIN,
status: FanavaranAuditStatus.FAILURE,
requestUrl: this.loginUrl,
httpStatus: isAxiosError(error) ? error.response?.status : undefined,
requestMethod: "POST",
httpStatus: exchange.httpStatus,
requestHeaders: exchange.requestHeaders,
requestBody: "",
responseHeaders: exchange.responseHeaders,
responseBody: exchange.responseBody,
errorMessage: this.fanavaranAuditService.extractErrorMessage(error),
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
durationMs: Date.now() - startedAt,