forked from Yara724/api
fanavaran duplication request problems fixed.
This commit is contained in:
@@ -4290,20 +4290,24 @@ export class ClaimRequestManagementService {
|
|||||||
const policyInquiryUrl = `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/common/Policies/inquiry-my-policies?InsuranceLineId=5&NationalCode=${nationalCodeOfInsurer}`;
|
const policyInquiryUrl = `https://apimanager.iraneit.com/BimeApiManager/api/BimeApi/v2.0/common/Policies/inquiry-my-policies?InsuranceLineId=5&NationalCode=${nationalCodeOfInsurer}`;
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
|
||||||
|
const requestMeta = {
|
||||||
|
corpId: config.corpId,
|
||||||
|
contractId: config.contractId,
|
||||||
|
location: config.location,
|
||||||
|
nationalCode: this.fanavaranAuditService.maskNationalCode(
|
||||||
|
nationalCodeOfInsurer,
|
||||||
|
),
|
||||||
|
InsuranceLineId: 5,
|
||||||
|
};
|
||||||
|
|
||||||
if (auditSession) {
|
if (auditSession) {
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.POLICY_INQUIRY,
|
step: FanavaranAuditStep.POLICY_INQUIRY,
|
||||||
status: FanavaranAuditStatus.STARTED,
|
status: FanavaranAuditStatus.STARTED,
|
||||||
requestUrl: policyInquiryUrl,
|
requestUrl: policyInquiryUrl,
|
||||||
requestMeta: {
|
requestMethod: "GET",
|
||||||
corpId: config.corpId,
|
requestMeta,
|
||||||
contractId: config.contractId,
|
|
||||||
location: config.location,
|
|
||||||
nationalCode: this.fanavaranAuditService.maskNationalCode(
|
|
||||||
nationalCodeOfInsurer,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4313,6 +4317,10 @@ export class ClaimRequestManagementService {
|
|||||||
clientKey,
|
clientKey,
|
||||||
auditSession,
|
auditSession,
|
||||||
);
|
);
|
||||||
|
const requestHeaders = {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`${logPrefix} Calling policy inquiry API for nationalCode: ${nationalCodeOfInsurer}`,
|
`${logPrefix} Calling policy inquiry API for nationalCode: ${nationalCodeOfInsurer}`,
|
||||||
@@ -4320,10 +4328,7 @@ export class ClaimRequestManagementService {
|
|||||||
|
|
||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
this.httpService.get(policyInquiryUrl, {
|
this.httpService.get(policyInquiryUrl, {
|
||||||
headers: {
|
headers: requestHeaders,
|
||||||
...headers,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -4351,17 +4356,27 @@ export class ClaimRequestManagementService {
|
|||||||
this.fanavaranAuthService.clearBackoff(clientKey);
|
this.fanavaranAuthService.clearBackoff(clientKey);
|
||||||
|
|
||||||
if (auditSession) {
|
if (auditSession) {
|
||||||
|
const exchange = this.fanavaranAuditService.captureAxiosExchange(
|
||||||
|
response,
|
||||||
|
requestHeaders,
|
||||||
|
);
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.POLICY_INQUIRY,
|
step: FanavaranAuditStep.POLICY_INQUIRY,
|
||||||
status: FanavaranAuditStatus.SUCCESS,
|
status: FanavaranAuditStatus.SUCCESS,
|
||||||
requestUrl: policyInquiryUrl,
|
requestUrl: policyInquiryUrl,
|
||||||
|
requestMethod: "GET",
|
||||||
httpStatus: response.status,
|
httpStatus: response.status,
|
||||||
|
requestHeaders: exchange.requestHeaders,
|
||||||
|
requestMeta,
|
||||||
|
responseHeaders: exchange.responseHeaders,
|
||||||
|
responseBody: exchange.responseBody,
|
||||||
responseMeta: {
|
responseMeta: {
|
||||||
policyId: selectedPolicy.policyId,
|
policyId: selectedPolicy.policyId,
|
||||||
policyEndDate: selectedPolicy.endDate,
|
policyEndDate: selectedPolicy.endDate,
|
||||||
policyEndDateGregorian: selectedPolicy.endDateGregorian,
|
policyEndDateGregorian: selectedPolicy.endDateGregorian,
|
||||||
policyCount,
|
policyCount,
|
||||||
|
fromCache: false,
|
||||||
},
|
},
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
});
|
});
|
||||||
@@ -4372,12 +4387,19 @@ export class ClaimRequestManagementService {
|
|||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : "unknown policy inquiry error";
|
error instanceof Error ? error.message : "unknown policy inquiry error";
|
||||||
if (auditSession) {
|
if (auditSession) {
|
||||||
|
const exchange =
|
||||||
|
this.fanavaranAuditService.captureAxiosExchange(error);
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.POLICY_INQUIRY,
|
step: FanavaranAuditStep.POLICY_INQUIRY,
|
||||||
status: FanavaranAuditStatus.FAILURE,
|
status: FanavaranAuditStatus.FAILURE,
|
||||||
requestUrl: policyInquiryUrl,
|
requestUrl: policyInquiryUrl,
|
||||||
httpStatus: isAxiosError(error) ? error.response?.status : undefined,
|
requestMethod: "GET",
|
||||||
|
httpStatus: exchange.httpStatus,
|
||||||
|
requestHeaders: exchange.requestHeaders,
|
||||||
|
requestMeta,
|
||||||
|
responseHeaders: exchange.responseHeaders,
|
||||||
|
responseBody: exchange.responseBody,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
@@ -4624,7 +4646,9 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cachedPolicyId =
|
const cachedPolicyId =
|
||||||
(claimCase as any)?.fanavaranSync?.baseClaim?.policyId ?? null;
|
(claimCase as any)?.fanavaranSync?.baseClaim?.policyId ??
|
||||||
|
(claimCase as any)?.fanavaranSync?.baseClaim?.lastPayload?.PolicyId ??
|
||||||
|
null;
|
||||||
|
|
||||||
const payload = await this.buildFanavaranSubmitPayload({
|
const payload = await this.buildFanavaranSubmitPayload({
|
||||||
accidentReason: selectedAccidentReason,
|
accidentReason: selectedAccidentReason,
|
||||||
@@ -4674,6 +4698,7 @@ export class ClaimRequestManagementService {
|
|||||||
if (cachedPolicyId != null && !forceRefreshPolicy) {
|
if (cachedPolicyId != null && !forceRefreshPolicy) {
|
||||||
debug.steps.policyIdFromCache = true;
|
debug.steps.policyIdFromCache = true;
|
||||||
debug.values.policyId = Number(cachedPolicyId);
|
debug.values.policyId = Number(cachedPolicyId);
|
||||||
|
// Silent reuse — no Fanavaran HTTP and no audit noise on warm preview.
|
||||||
return Number(cachedPolicyId);
|
return Number(cachedPolicyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5018,7 +5043,37 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const profile = getFanavaranClientProfile(clientKey);
|
const profile = getFanavaranClientProfile(clientKey);
|
||||||
const claimCase = await this.claimCaseDbService.findById(claimCaseId);
|
let claimCase = await this.claimCaseDbService.findById(claimCaseId);
|
||||||
|
if (!claimCase?.claimId) {
|
||||||
|
try {
|
||||||
|
await this.ensureFanavaranBaseClaim(
|
||||||
|
claimCaseId,
|
||||||
|
clientKey,
|
||||||
|
FanavaranAuditSource.SUBMIT,
|
||||||
|
);
|
||||||
|
claimCase = await this.claimCaseDbService.findById(claimCaseId);
|
||||||
|
} catch (error) {
|
||||||
|
const warning = this.extractFanavaranErrorMessage(error);
|
||||||
|
return {
|
||||||
|
clientKey,
|
||||||
|
claimCaseId,
|
||||||
|
totalLocalImages: candidates.length,
|
||||||
|
skippedAlreadySubmitted: candidates.length - pending.length,
|
||||||
|
attempted: 0,
|
||||||
|
submitted: 0,
|
||||||
|
failed: 0,
|
||||||
|
skipped: pending.length,
|
||||||
|
warning,
|
||||||
|
results: pending.map((c) => ({
|
||||||
|
attempted: false,
|
||||||
|
submitted: false,
|
||||||
|
skipped: true,
|
||||||
|
skipReason: warning,
|
||||||
|
fileName: c.fileName,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!claimCase?.claimId) {
|
if (!claimCase?.claimId) {
|
||||||
return {
|
return {
|
||||||
clientKey,
|
clientKey,
|
||||||
@@ -5219,13 +5274,14 @@ export class ClaimRequestManagementService {
|
|||||||
step: FanavaranAuditStep.SUBMIT_ATTACHMENT,
|
step: FanavaranAuditStep.SUBMIT_ATTACHMENT,
|
||||||
status: FanavaranAuditStatus.STARTED,
|
status: FanavaranAuditStatus.STARTED,
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
|
requestMethod: "POST",
|
||||||
|
requestBody: content,
|
||||||
requestMeta: {
|
requestMeta: {
|
||||||
claimId: claimCase.claimId,
|
claimId: claimCase.claimId,
|
||||||
claimNo: claimCase.claimNo,
|
claimNo: claimCase.claimNo,
|
||||||
dmgCaseId: claimCase.dmgCaseId,
|
dmgCaseId: claimCase.dmgCaseId,
|
||||||
fileName,
|
fileName,
|
||||||
source: file.source,
|
source: file.source,
|
||||||
content,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -5234,18 +5290,25 @@ export class ClaimRequestManagementService {
|
|||||||
content,
|
content,
|
||||||
[{ path: filePath, fileName }],
|
[{ path: filePath, fileName }],
|
||||||
clientKey,
|
clientKey,
|
||||||
|
auditSession,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`${logPrefix} Fanavaran response: ${JSON.stringify(response.data)}`);
|
this.logger.log(`${logPrefix} Fanavaran response: ${JSON.stringify(response.data)}`);
|
||||||
|
|
||||||
const fileId = response.data?.Id;
|
const fileId = response.data?.Id;
|
||||||
|
const exchange =
|
||||||
|
this.fanavaranAuditService.captureAxiosExchange(response);
|
||||||
|
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.SUBMIT_ATTACHMENT,
|
step: FanavaranAuditStep.SUBMIT_ATTACHMENT,
|
||||||
status: FanavaranAuditStatus.SUCCESS,
|
status: FanavaranAuditStatus.SUCCESS,
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
|
requestMethod: "POST",
|
||||||
httpStatus: response.status,
|
httpStatus: response.status,
|
||||||
|
requestBody: content,
|
||||||
|
responseHeaders: exchange.responseHeaders,
|
||||||
|
responseBody: exchange.responseBody,
|
||||||
responseMeta: { claimId: claimCase.claimId, fileId, fileName },
|
responseMeta: { claimId: claimCase.claimId, fileId, fileName },
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
});
|
});
|
||||||
@@ -5693,6 +5756,11 @@ export class ClaimRequestManagementService {
|
|||||||
bodyOverride?: Record<string, unknown>,
|
bodyOverride?: Record<string, unknown>,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
await this.ensureFanavaranDamageCase(
|
||||||
|
claimCaseId,
|
||||||
|
clientKey,
|
||||||
|
FanavaranAuditSource.SUBMIT,
|
||||||
|
);
|
||||||
const claimCase = await this.claimCaseDbService.findById(claimCaseId);
|
const claimCase = await this.claimCaseDbService.findById(claimCaseId);
|
||||||
if (!claimCase) throw new NotFoundException("Claim case not found");
|
if (!claimCase) throw new NotFoundException("Claim case not found");
|
||||||
let payload: Record<string, unknown>;
|
let payload: Record<string, unknown>;
|
||||||
@@ -5728,12 +5796,30 @@ export class ClaimRequestManagementService {
|
|||||||
payload: Record<string, unknown>,
|
payload: Record<string, unknown>,
|
||||||
auditSource: FanavaranAuditSource,
|
auditSource: FanavaranAuditSource,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const claimCase = await this.claimCaseDbService.findById(claimCaseId);
|
let claimCase = await this.claimCaseDbService.findById(claimCaseId);
|
||||||
|
if (!claimCase?.claimId || claimCase.dmgCaseId == null) {
|
||||||
|
await this.ensureFanavaranDamageCase(
|
||||||
|
claimCaseId,
|
||||||
|
clientKey,
|
||||||
|
auditSource,
|
||||||
|
);
|
||||||
|
claimCase = await this.claimCaseDbService.findById(claimCaseId);
|
||||||
|
}
|
||||||
if (!claimCase?.claimId) {
|
if (!claimCase?.claimId) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Fanavaran claimId is required before submitting expertise",
|
"Fanavaran claimId is required before submitting expertise",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (claimCase.expertiseId != null) {
|
||||||
|
this.logger.log(
|
||||||
|
`[executeFanavaranExpertiseSubmit] Already have expertiseId=${claimCase.expertiseId}; skipping Fanavaran POST`,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
(claimCase as any)?.fanavaranSync?.expertise?.response ?? {
|
||||||
|
Id: claimCase.expertiseId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
const url = `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/expertise`;
|
const url = `${this.FANAVARAN_SUBMIT_URL}/${claimCase.claimId}/expertise`;
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const auditSession: FanavaranAuditSession = {
|
const auditSession: FanavaranAuditSession = {
|
||||||
@@ -5748,19 +5834,32 @@ export class ClaimRequestManagementService {
|
|||||||
step: FanavaranAuditStep.SUBMIT_EXPERTISE,
|
step: FanavaranAuditStep.SUBMIT_EXPERTISE,
|
||||||
status: FanavaranAuditStatus.STARTED,
|
status: FanavaranAuditStatus.STARTED,
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
requestMeta: { claimId: claimCase.claimId, payload },
|
requestMethod: "POST",
|
||||||
|
requestBody: payload,
|
||||||
|
requestMeta: { claimId: claimCase.claimId },
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.postFanavaranJson(url, payload, clientKey);
|
const response = await this.postFanavaranJson(
|
||||||
|
url,
|
||||||
|
payload,
|
||||||
|
clientKey,
|
||||||
|
auditSession,
|
||||||
|
);
|
||||||
const expertiseId = response.data?.Id;
|
const expertiseId = response.data?.Id;
|
||||||
|
const exchange =
|
||||||
|
this.fanavaranAuditService.captureAxiosExchange(response);
|
||||||
|
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.SUBMIT_EXPERTISE,
|
step: FanavaranAuditStep.SUBMIT_EXPERTISE,
|
||||||
status: FanavaranAuditStatus.SUCCESS,
|
status: FanavaranAuditStatus.SUCCESS,
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
|
requestMethod: "POST",
|
||||||
httpStatus: response.status,
|
httpStatus: response.status,
|
||||||
|
requestBody: payload,
|
||||||
|
responseHeaders: exchange.responseHeaders,
|
||||||
|
responseBody: exchange.responseBody,
|
||||||
responseMeta: { claimId: claimCase.claimId, expertiseId },
|
responseMeta: { claimId: claimCase.claimId, expertiseId },
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
});
|
});
|
||||||
@@ -5781,12 +5880,17 @@ export class ClaimRequestManagementService {
|
|||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const exchange = this.fanavaranAuditService.captureAxiosExchange(error);
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.SUBMIT_EXPERTISE,
|
step: FanavaranAuditStep.SUBMIT_EXPERTISE,
|
||||||
status: FanavaranAuditStatus.FAILURE,
|
status: FanavaranAuditStatus.FAILURE,
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
httpStatus: isAxiosError(error) ? error.response?.status : undefined,
|
requestMethod: "POST",
|
||||||
|
httpStatus: exchange.httpStatus,
|
||||||
|
requestBody: payload,
|
||||||
|
responseHeaders: exchange.responseHeaders,
|
||||||
|
responseBody: exchange.responseBody,
|
||||||
errorMessage: this.fanavaranAuditService.extractErrorMessage(error),
|
errorMessage: this.fanavaranAuditService.extractErrorMessage(error),
|
||||||
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
@@ -5952,13 +6056,104 @@ export class ClaimRequestManagementService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft-ensure Fanavaran base claim exists before a later stage (damage /
|
||||||
|
* attachments / expertise). Reuses claimId when present; otherwise submits
|
||||||
|
* GEN.03 once. Does not invent a fake claimId on failure.
|
||||||
|
*/
|
||||||
|
private async ensureFanavaranBaseClaim(
|
||||||
|
claimCaseId: string,
|
||||||
|
clientKey: FanavaranClientKey,
|
||||||
|
auditSource: FanavaranAuditSource,
|
||||||
|
): Promise<{ claimId: number; claimNo?: string; created: boolean }> {
|
||||||
|
const existing = await this.claimCaseDbService.findById(claimCaseId);
|
||||||
|
if (!existing) {
|
||||||
|
throw new NotFoundException("Claim case not found");
|
||||||
|
}
|
||||||
|
if (existing.claimId != null) {
|
||||||
|
return {
|
||||||
|
claimId: Number(existing.claimId),
|
||||||
|
claimNo:
|
||||||
|
existing.claimNo != null ? String(existing.claimNo) : undefined,
|
||||||
|
created: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`[Fanavaran ${clientKey}] Soft-ensuring base claim before next stage claimCaseId=${claimCaseId}`,
|
||||||
|
);
|
||||||
|
await this.executeFanavaranV2Submit(
|
||||||
|
claimCaseId,
|
||||||
|
clientKey,
|
||||||
|
undefined,
|
||||||
|
auditSource,
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshed = await this.claimCaseDbService.findById(claimCaseId);
|
||||||
|
if (refreshed?.claimId == null) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Fanavaran base claim is missing and could not be created before the next stage. Fix base claim first.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
claimId: Number(refreshed.claimId),
|
||||||
|
claimNo:
|
||||||
|
refreshed.claimNo != null ? String(refreshed.claimNo) : undefined,
|
||||||
|
created: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft-ensure damage case exists (creates base claim first if needed).
|
||||||
|
*/
|
||||||
|
private async ensureFanavaranDamageCase(
|
||||||
|
claimCaseId: string,
|
||||||
|
clientKey: FanavaranClientKey,
|
||||||
|
auditSource: FanavaranAuditSource,
|
||||||
|
selectedParts?: unknown[],
|
||||||
|
): Promise<{ claimId: number; dmgCaseId: number; created: boolean }> {
|
||||||
|
await this.ensureFanavaranBaseClaim(claimCaseId, clientKey, auditSource);
|
||||||
|
|
||||||
|
const existing = await this.claimCaseDbService.findById(claimCaseId);
|
||||||
|
if (existing?.dmgCaseId != null && existing.claimId != null) {
|
||||||
|
return {
|
||||||
|
claimId: Number(existing.claimId),
|
||||||
|
dmgCaseId: Number(existing.dmgCaseId),
|
||||||
|
created: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`[Fanavaran ${clientKey}] Soft-ensuring damage case before next stage claimCaseId=${claimCaseId}`,
|
||||||
|
);
|
||||||
|
await this.executeFanavaranDamageCaseSubmit(
|
||||||
|
claimCaseId,
|
||||||
|
clientKey,
|
||||||
|
selectedParts ?? existing?.damage?.selectedParts ?? [],
|
||||||
|
auditSource,
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshed = await this.claimCaseDbService.findById(claimCaseId);
|
||||||
|
if (refreshed?.claimId == null || refreshed.dmgCaseId == null) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Fanavaran damage case is missing and could not be created before the next stage.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
claimId: Number(refreshed.claimId),
|
||||||
|
dmgCaseId: Number(refreshed.dmgCaseId),
|
||||||
|
created: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async postFanavaranJson(
|
private async postFanavaranJson(
|
||||||
url: string,
|
url: string,
|
||||||
payload: Record<string, unknown>,
|
payload: Record<string, unknown>,
|
||||||
clientKey: FanavaranClientKey,
|
clientKey: FanavaranClientKey,
|
||||||
|
auditSession?: FanavaranAuditSession,
|
||||||
) {
|
) {
|
||||||
this.fanavaranAuthService.assertNotInBackoff(clientKey);
|
this.fanavaranAuthService.assertNotInBackoff(clientKey);
|
||||||
const headers = await this.getFanavaranAuthHeaders(clientKey);
|
const headers = await this.getFanavaranAuthHeaders(clientKey, auditSession);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
@@ -5982,9 +6177,10 @@ export class ClaimRequestManagementService {
|
|||||||
content: Record<string, unknown>,
|
content: Record<string, unknown>,
|
||||||
files: Array<{ path: string; fileName: string }>,
|
files: Array<{ path: string; fileName: string }>,
|
||||||
clientKey: FanavaranClientKey,
|
clientKey: FanavaranClientKey,
|
||||||
|
auditSession?: FanavaranAuditSession,
|
||||||
) {
|
) {
|
||||||
this.fanavaranAuthService.assertNotInBackoff(clientKey);
|
this.fanavaranAuthService.assertNotInBackoff(clientKey);
|
||||||
const headers = await this.getFanavaranAuthHeaders(clientKey);
|
const headers = await this.getFanavaranAuthHeaders(clientKey, auditSession);
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
|
|
||||||
form.append("Param", JSON.stringify(content), {
|
form.append("Param", JSON.stringify(content), {
|
||||||
@@ -6043,13 +6239,27 @@ export class ClaimRequestManagementService {
|
|||||||
bodyOverride?: Record<string, unknown>,
|
bodyOverride?: Record<string, unknown>,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const profile = getFanavaranClientProfile(clientKey);
|
const profile = getFanavaranClientProfile(clientKey);
|
||||||
const claimCase = await this.claimCaseDbService.findById(claimCaseId);
|
let claimCase = await this.claimCaseDbService.findById(claimCaseId);
|
||||||
if (!claimCase) {
|
if (!claimCase) {
|
||||||
throw new NotFoundException("Claim case not found");
|
throw new NotFoundException("Claim case not found");
|
||||||
}
|
}
|
||||||
if (!claimCase.claimId) {
|
if (!claimCase.claimId) {
|
||||||
throw new BadRequestException(
|
await this.ensureFanavaranBaseClaim(claimCaseId, clientKey, auditSource);
|
||||||
"Fanavaran claimId is required before submitting damage case",
|
claimCase = await this.claimCaseDbService.findById(claimCaseId);
|
||||||
|
if (!claimCase?.claimId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Fanavaran claimId is required before submitting damage case",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (claimCase.dmgCaseId != null && !bodyOverride) {
|
||||||
|
this.logger.log(
|
||||||
|
`[executeFanavaranDamageCaseSubmit] Already have dmgCaseId=${claimCase.dmgCaseId}; skipping Fanavaran POST`,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
(claimCase as any)?.fanavaranSync?.damageCase?.response ?? {
|
||||||
|
Id: claimCase.dmgCaseId,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!claimCase.blameRequestId) {
|
if (!claimCase.blameRequestId) {
|
||||||
@@ -6092,19 +6302,32 @@ export class ClaimRequestManagementService {
|
|||||||
step: FanavaranAuditStep.SUBMIT_DAMAGE_CASE,
|
step: FanavaranAuditStep.SUBMIT_DAMAGE_CASE,
|
||||||
status: FanavaranAuditStatus.STARTED,
|
status: FanavaranAuditStatus.STARTED,
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
requestMeta: { claimId: claimCase.claimId, payload },
|
requestMethod: "POST",
|
||||||
|
requestBody: payload,
|
||||||
|
requestMeta: { claimId: claimCase.claimId },
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.postFanavaranJson(url, payload, clientKey);
|
const response = await this.postFanavaranJson(
|
||||||
|
url,
|
||||||
|
payload,
|
||||||
|
clientKey,
|
||||||
|
auditSession,
|
||||||
|
);
|
||||||
const dmgCaseId = response.data?.Id;
|
const dmgCaseId = response.data?.Id;
|
||||||
|
const exchange =
|
||||||
|
this.fanavaranAuditService.captureAxiosExchange(response);
|
||||||
|
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.SUBMIT_DAMAGE_CASE,
|
step: FanavaranAuditStep.SUBMIT_DAMAGE_CASE,
|
||||||
status: FanavaranAuditStatus.SUCCESS,
|
status: FanavaranAuditStatus.SUCCESS,
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
|
requestMethod: "POST",
|
||||||
httpStatus: response.status,
|
httpStatus: response.status,
|
||||||
|
requestBody: payload,
|
||||||
|
responseHeaders: exchange.responseHeaders,
|
||||||
|
responseBody: exchange.responseBody,
|
||||||
responseMeta: { dmgCaseId, claimId: claimCase.claimId },
|
responseMeta: { dmgCaseId, claimId: claimCase.claimId },
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
});
|
});
|
||||||
@@ -6134,12 +6357,17 @@ export class ClaimRequestManagementService {
|
|||||||
this.logger.error(
|
this.logger.error(
|
||||||
`[executeFanavaranDamageCaseSubmit] FAILED claimCaseId=${claimCaseId} claimId=${claimCase.claimId} payload.DriverId=${payload.DriverId ?? "NULL"} error: ${errDetail}`,
|
`[executeFanavaranDamageCaseSubmit] FAILED claimCaseId=${claimCaseId} claimId=${claimCase.claimId} payload.DriverId=${payload.DriverId ?? "NULL"} error: ${errDetail}`,
|
||||||
);
|
);
|
||||||
|
const exchange = this.fanavaranAuditService.captureAxiosExchange(error);
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.SUBMIT_DAMAGE_CASE,
|
step: FanavaranAuditStep.SUBMIT_DAMAGE_CASE,
|
||||||
status: FanavaranAuditStatus.FAILURE,
|
status: FanavaranAuditStatus.FAILURE,
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
httpStatus: isAxiosError(error) ? error.response?.status : undefined,
|
requestMethod: "POST",
|
||||||
|
httpStatus: exchange.httpStatus,
|
||||||
|
requestBody: payload,
|
||||||
|
responseHeaders: exchange.responseHeaders,
|
||||||
|
responseBody: exchange.responseBody,
|
||||||
errorMessage: this.fanavaranAuditService.extractErrorMessage(error),
|
errorMessage: this.fanavaranAuditService.extractErrorMessage(error),
|
||||||
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
@@ -6830,6 +7058,19 @@ export class ClaimRequestManagementService {
|
|||||||
`${logPrefix} Starting submission for claimCaseId: ${claimCaseId}`,
|
`${logPrefix} Starting submission for claimCaseId: ${claimCaseId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const existing = await this.claimCaseDbService.findById(claimCaseId);
|
||||||
|
if (existing?.claimId != null && !bodyOverride) {
|
||||||
|
this.logger.log(
|
||||||
|
`${logPrefix} Base claim already exists claimId=${existing.claimId}; skipping Fanavaran POST`,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
(existing as any)?.fanavaranSync?.baseClaim?.response ?? {
|
||||||
|
Id: existing.claimId,
|
||||||
|
ClaimNo: existing.claimNo,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const auditSession: FanavaranAuditSession = {
|
const auditSession: FanavaranAuditSession = {
|
||||||
trackingCode: this.fanavaranAuditService.generateTrackingCode(),
|
trackingCode: this.fanavaranAuditService.generateTrackingCode(),
|
||||||
clientKey,
|
clientKey,
|
||||||
@@ -6868,11 +7109,18 @@ export class ClaimRequestManagementService {
|
|||||||
auditSession,
|
auditSession,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const requestHeaders = {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.SUBMIT_CLAIM,
|
step: FanavaranAuditStep.SUBMIT_CLAIM,
|
||||||
status: FanavaranAuditStatus.STARTED,
|
status: FanavaranAuditStatus.STARTED,
|
||||||
requestUrl: this.FANAVARAN_SUBMIT_URL,
|
requestUrl: this.FANAVARAN_SUBMIT_URL,
|
||||||
|
requestMethod: "POST",
|
||||||
|
requestHeaders,
|
||||||
|
requestBody: fanavaranData,
|
||||||
requestMeta: { policyId: fanavaranData?.PolicyId ?? null },
|
requestMeta: { policyId: fanavaranData?.PolicyId ?? null },
|
||||||
});
|
});
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
@@ -6880,10 +7128,7 @@ export class ClaimRequestManagementService {
|
|||||||
try {
|
try {
|
||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
this.httpService.post(this.FANAVARAN_SUBMIT_URL, fanavaranData, {
|
this.httpService.post(this.FANAVARAN_SUBMIT_URL, fanavaranData, {
|
||||||
headers: {
|
headers: requestHeaders,
|
||||||
...headers,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -6895,12 +7140,21 @@ export class ClaimRequestManagementService {
|
|||||||
|
|
||||||
this.fanavaranAuthService.clearBackoff(clientKey);
|
this.fanavaranAuthService.clearBackoff(clientKey);
|
||||||
|
|
||||||
|
const exchange = this.fanavaranAuditService.captureAxiosExchange(
|
||||||
|
response,
|
||||||
|
requestHeaders,
|
||||||
|
);
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.SUBMIT_CLAIM,
|
step: FanavaranAuditStep.SUBMIT_CLAIM,
|
||||||
status: FanavaranAuditStatus.SUCCESS,
|
status: FanavaranAuditStatus.SUCCESS,
|
||||||
requestUrl: this.FANAVARAN_SUBMIT_URL,
|
requestUrl: this.FANAVARAN_SUBMIT_URL,
|
||||||
|
requestMethod: "POST",
|
||||||
httpStatus: response.status,
|
httpStatus: response.status,
|
||||||
|
requestHeaders: exchange.requestHeaders,
|
||||||
|
requestBody: fanavaranData,
|
||||||
|
responseHeaders: exchange.responseHeaders,
|
||||||
|
responseBody: exchange.responseBody,
|
||||||
responseMeta: {
|
responseMeta: {
|
||||||
claimId: response.data?.Id,
|
claimId: response.data?.Id,
|
||||||
claimNo: response.data?.ClaimNo,
|
claimNo: response.data?.ClaimNo,
|
||||||
@@ -6946,12 +7200,21 @@ export class ClaimRequestManagementService {
|
|||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.fanavaranAuthService.registerFailure(clientKey, error);
|
this.fanavaranAuthService.registerFailure(clientKey, error);
|
||||||
|
const exchange = this.fanavaranAuditService.captureAxiosExchange(
|
||||||
|
error,
|
||||||
|
requestHeaders,
|
||||||
|
);
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.SUBMIT_CLAIM,
|
step: FanavaranAuditStep.SUBMIT_CLAIM,
|
||||||
status: FanavaranAuditStatus.FAILURE,
|
status: FanavaranAuditStatus.FAILURE,
|
||||||
requestUrl: this.FANAVARAN_SUBMIT_URL,
|
requestUrl: this.FANAVARAN_SUBMIT_URL,
|
||||||
httpStatus: isAxiosError(error) ? error.response?.status : undefined,
|
requestMethod: "POST",
|
||||||
|
httpStatus: exchange.httpStatus,
|
||||||
|
requestHeaders: exchange.requestHeaders,
|
||||||
|
requestBody: fanavaranData,
|
||||||
|
responseHeaders: exchange.responseHeaders,
|
||||||
|
responseBody: exchange.responseBody,
|
||||||
errorMessage: this.fanavaranAuditService.extractErrorMessage(error),
|
errorMessage: this.fanavaranAuditService.extractErrorMessage(error),
|
||||||
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
export type FanavaranClientKey = "parsian" | "tejaratno";
|
export type FanavaranClientKey = "parsian" | "tejaratno" | "moallem";
|
||||||
|
|
||||||
export const FANAVARAN_CLIENT_KEYS: readonly FanavaranClientKey[] = [
|
export const FANAVARAN_CLIENT_KEYS: readonly FanavaranClientKey[] = [
|
||||||
"parsian",
|
"parsian",
|
||||||
"tejaratno",
|
"tejaratno",
|
||||||
|
"moallem",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
/** Swagger `@ApiParam({ enum })` value — keep in sync with {@link FANAVARAN_CLIENT_KEYS}. */
|
||||||
|
export const FANAVARAN_CLIENT_SWAGGER_ENUM: FanavaranClientKey[] = [
|
||||||
|
...FANAVARAN_CLIENT_KEYS,
|
||||||
|
];
|
||||||
|
|
||||||
export function isFanavaranClientKey(
|
export function isFanavaranClientKey(
|
||||||
value: string,
|
value: string,
|
||||||
): value is FanavaranClientKey {
|
): value is FanavaranClientKey {
|
||||||
const normalized = value?.trim().toLowerCase();
|
const normalized = value?.trim().toLowerCase();
|
||||||
return normalized === "parsian" || normalized === "tejaratno";
|
return (FANAVARAN_CLIENT_KEYS as readonly string[]).includes(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeFanavaranClientKey(value: string): FanavaranClientKey {
|
export function normalizeFanavaranClientKey(value: string): FanavaranClientKey {
|
||||||
const normalized = value?.trim().toLowerCase();
|
const normalized = value?.trim().toLowerCase();
|
||||||
if (normalized === "parsian" || normalized === "tejaratno") {
|
if (isFanavaranClientKey(normalized)) {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -56,6 +62,29 @@ export interface FanavaranClientProfile {
|
|||||||
defaults: FanavaranPayloadDefaults;
|
defaults: FanavaranPayloadDefaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared codebook-ish defaults used when a tenant has not supplied its own
|
||||||
|
* ClaimExpertId / plaque ids yet. Moallem auth is real; ClaimExpertId may need
|
||||||
|
* a Moallem-specific value from Fanavaran lookups after first deploy.
|
||||||
|
*/
|
||||||
|
const SHARED_FANAVARAN_DEFAULTS: FanavaranPayloadDefaults = {
|
||||||
|
AccidentCityId: 701,
|
||||||
|
AccidentReportTypeId: 155,
|
||||||
|
AccidentVehicleUsedId: 1,
|
||||||
|
ClaimExpertId: 4543092,
|
||||||
|
ExpertiseClaimExpertId: 4543092,
|
||||||
|
CompensationReferenceId: 167,
|
||||||
|
CulpritLicenceTypeId: 2,
|
||||||
|
CulpritTypeId: 337,
|
||||||
|
DmgCaseTypeId: 175,
|
||||||
|
DmgHistoryStatus: 5214,
|
||||||
|
PlaqueKindId: 8,
|
||||||
|
PlaqueSampleId: 10,
|
||||||
|
DriverIsOwner: 0,
|
||||||
|
FaultPercent: 100,
|
||||||
|
ClaimFileTypeId: 23,
|
||||||
|
};
|
||||||
|
|
||||||
const FANAVARAN_CLIENT_PROFILES: Record<
|
const FANAVARAN_CLIENT_PROFILES: Record<
|
||||||
FanavaranClientKey,
|
FanavaranClientKey,
|
||||||
FanavaranClientProfile
|
FanavaranClientProfile
|
||||||
@@ -72,20 +101,9 @@ const FANAVARAN_CLIENT_PROFILES: Record<
|
|||||||
location: "100",
|
location: "100",
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
AccidentCityId: 701,
|
...SHARED_FANAVARAN_DEFAULTS,
|
||||||
AccidentReportTypeId: 155,
|
|
||||||
AccidentVehicleUsedId: 1,
|
|
||||||
ClaimExpertId: 4543092,
|
ClaimExpertId: 4543092,
|
||||||
ExpertiseClaimExpertId: 4543092,
|
ExpertiseClaimExpertId: 4543092,
|
||||||
CompensationReferenceId: 167,
|
|
||||||
CulpritLicenceTypeId: 2,
|
|
||||||
CulpritTypeId: 337,
|
|
||||||
DmgCaseTypeId: 175,
|
|
||||||
DmgHistoryStatus: 5214,
|
|
||||||
PlaqueKindId: 8,
|
|
||||||
PlaqueSampleId: 10,
|
|
||||||
DriverIsOwner: 0,
|
|
||||||
FaultPercent: 100,
|
|
||||||
ClaimFileTypeId: 23,
|
ClaimFileTypeId: 23,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -101,29 +119,35 @@ const FANAVARAN_CLIENT_PROFILES: Record<
|
|||||||
location: "210050",
|
location: "210050",
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
AccidentCityId: 701,
|
...SHARED_FANAVARAN_DEFAULTS,
|
||||||
AccidentReportTypeId: 155,
|
|
||||||
AccidentVehicleUsedId: 1,
|
|
||||||
ClaimExpertId: 154,
|
ClaimExpertId: 154,
|
||||||
ExpertiseClaimExpertId: 29,
|
ExpertiseClaimExpertId: 29,
|
||||||
CompensationReferenceId: 167,
|
|
||||||
CulpritLicenceTypeId: 2,
|
|
||||||
CulpritTypeId: 337,
|
|
||||||
DmgCaseTypeId: 175,
|
|
||||||
DmgHistoryStatus: 5214,
|
|
||||||
PlaqueKindId: 8,
|
|
||||||
PlaqueSampleId: 10,
|
|
||||||
DriverIsOwner: 0,
|
|
||||||
FaultPercent: 100,
|
|
||||||
ClaimFileTypeId: 70,
|
ClaimFileTypeId: 70,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
moallem: {
|
||||||
|
key: "moallem",
|
||||||
|
auth: {
|
||||||
|
appName: "ItTalie",
|
||||||
|
secret: "itT@l!3@api",
|
||||||
|
username: "itTalieUser",
|
||||||
|
password: "itT@l!3@user",
|
||||||
|
corpId: "5650",
|
||||||
|
contractId: "304",
|
||||||
|
location: "30900",
|
||||||
|
},
|
||||||
|
// ClaimExpertId / ClaimFileTypeId not yet confirmed for Moallem — start from
|
||||||
|
// shared Fanavaran codebook defaults and override after lookup.
|
||||||
|
defaults: {
|
||||||
|
...SHARED_FANAVARAN_DEFAULTS,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Resolve active Fanavaran tenant from env (`FANAVARAN_CLIENT`) with optional CLIENT_ID fallback. */
|
/** Resolve active Fanavaran tenant from env (`FANAVARAN_CLIENT`) with optional CLIENT_ID fallback. */
|
||||||
export function resolveFanavaranClientKey(): FanavaranClientKey {
|
export function resolveFanavaranClientKey(): FanavaranClientKey {
|
||||||
const explicit = process.env.FANAVARAN_CLIENT?.trim().toLowerCase();
|
const explicit = process.env.FANAVARAN_CLIENT?.trim().toLowerCase();
|
||||||
if (explicit === "parsian" || explicit === "tejaratno") {
|
if (explicit && isFanavaranClientKey(explicit)) {
|
||||||
return explicit;
|
return explicit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import { InjectModel } from "@nestjs/mongoose";
|
import { InjectModel } from "@nestjs/mongoose";
|
||||||
import { isAxiosError } from "axios";
|
import { isAxiosError, type AxiosResponse } from "axios";
|
||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { Model, Types } from "mongoose";
|
import { Model, Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
@@ -16,18 +16,54 @@ export interface RecordFanavaranAuditStepInput {
|
|||||||
step: FanavaranAuditStep;
|
step: FanavaranAuditStep;
|
||||||
status: FanavaranAuditStatus;
|
status: FanavaranAuditStatus;
|
||||||
requestUrl?: string;
|
requestUrl?: string;
|
||||||
|
requestMethod?: string;
|
||||||
httpStatus?: number;
|
httpStatus?: number;
|
||||||
|
requestHeaders?: Record<string, unknown>;
|
||||||
|
requestBody?: unknown;
|
||||||
requestMeta?: Record<string, unknown>;
|
requestMeta?: Record<string, unknown>;
|
||||||
|
responseHeaders?: Record<string, unknown>;
|
||||||
|
responseBody?: unknown;
|
||||||
responseMeta?: Record<string, unknown>;
|
responseMeta?: Record<string, unknown>;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
errorDetails?: Record<string, unknown>;
|
errorDetails?: Record<string, unknown>;
|
||||||
durationMs?: number;
|
durationMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FanavaranHttpExchange {
|
||||||
|
httpStatus?: number;
|
||||||
|
requestHeaders?: Record<string, unknown>;
|
||||||
|
responseHeaders?: Record<string, unknown>;
|
||||||
|
responseBody?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FanavaranAuditService {
|
export class FanavaranAuditService {
|
||||||
private readonly logger = new Logger(FanavaranAuditService.name);
|
private readonly logger = new Logger(FanavaranAuditService.name);
|
||||||
|
|
||||||
|
/** Max serialized chars kept for request/response bodies in audit docs. */
|
||||||
|
static readonly BODY_MAX_CHARS = 80_000;
|
||||||
|
|
||||||
|
private static readonly SENSITIVE_HEADER_KEYS = new Set([
|
||||||
|
"password",
|
||||||
|
"secret",
|
||||||
|
"authorization",
|
||||||
|
"authenticationtoken",
|
||||||
|
"apptoken",
|
||||||
|
"app-token",
|
||||||
|
"x-api-key",
|
||||||
|
"cookie",
|
||||||
|
"set-cookie",
|
||||||
|
]);
|
||||||
|
|
||||||
|
private static readonly SENSITIVE_BODY_KEYS = new Set([
|
||||||
|
"password",
|
||||||
|
"secret",
|
||||||
|
"authenticationtoken",
|
||||||
|
"apptoken",
|
||||||
|
"token",
|
||||||
|
"files",
|
||||||
|
]);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectModel(FanavaranAuditLog.name)
|
@InjectModel(FanavaranAuditLog.name)
|
||||||
private readonly auditModel: Model<FanavaranAuditLogDocument>,
|
private readonly auditModel: Model<FanavaranAuditLogDocument>,
|
||||||
@@ -47,6 +83,138 @@ export class FanavaranAuditService {
|
|||||||
return `${trimmed.slice(0, 3)}****${trimmed.slice(-2)}`;
|
return `${trimmed.slice(0, 3)}****${trimmed.slice(-2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maskToken(value: string): string {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed.length <= 8) {
|
||||||
|
return "****";
|
||||||
|
}
|
||||||
|
return `${trimmed.slice(0, 4)}…${trimmed.slice(-4)} (len=${trimmed.length})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizeHeaders(
|
||||||
|
headers?: Record<string, unknown> | null,
|
||||||
|
): Record<string, unknown> | undefined {
|
||||||
|
if (!headers || typeof headers !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [rawKey, rawValue] of Object.entries(headers)) {
|
||||||
|
if (rawValue === undefined) continue;
|
||||||
|
const key = String(rawKey);
|
||||||
|
const lower = key.toLowerCase();
|
||||||
|
if (FanavaranAuditService.SENSITIVE_HEADER_KEYS.has(lower)) {
|
||||||
|
out[key] =
|
||||||
|
typeof rawValue === "string"
|
||||||
|
? this.maskToken(rawValue)
|
||||||
|
: "***";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof rawValue === "string" ||
|
||||||
|
typeof rawValue === "number" ||
|
||||||
|
typeof rawValue === "boolean" ||
|
||||||
|
rawValue === null
|
||||||
|
) {
|
||||||
|
out[key] = rawValue;
|
||||||
|
} else {
|
||||||
|
out[key] = String(rawValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizeBody(body: unknown): unknown {
|
||||||
|
if (body === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const masked = this.maskSensitiveDeep(body);
|
||||||
|
try {
|
||||||
|
const serialized = JSON.stringify(masked);
|
||||||
|
if (serialized.length <= FanavaranAuditService.BODY_MAX_CHARS) {
|
||||||
|
return masked;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
_truncated: true,
|
||||||
|
maxChars: FanavaranAuditService.BODY_MAX_CHARS,
|
||||||
|
preview: serialized.slice(0, FanavaranAuditService.BODY_MAX_CHARS),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
const asString = String(masked);
|
||||||
|
return asString.length <= FanavaranAuditService.BODY_MAX_CHARS
|
||||||
|
? asString
|
||||||
|
: {
|
||||||
|
_truncated: true,
|
||||||
|
preview: asString.slice(0, FanavaranAuditService.BODY_MAX_CHARS),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract request/response headers + body from an Axios response or Axios
|
||||||
|
* error (uses `config.headers` when present). Values are returned raw;
|
||||||
|
* `recordStep` applies sanitisation before persistence.
|
||||||
|
*/
|
||||||
|
captureAxiosExchange(
|
||||||
|
source: unknown,
|
||||||
|
overrideRequestHeaders?: Record<string, unknown>,
|
||||||
|
): FanavaranHttpExchange {
|
||||||
|
const toPlainHeaders = (
|
||||||
|
headers?: Record<string, unknown> | null,
|
||||||
|
): Record<string, unknown> | undefined => {
|
||||||
|
if (!headers || typeof headers !== "object") return undefined;
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
|
if (value === undefined) continue;
|
||||||
|
out[key] =
|
||||||
|
typeof value === "string" ||
|
||||||
|
typeof value === "number" ||
|
||||||
|
typeof value === "boolean" ||
|
||||||
|
value === null
|
||||||
|
? value
|
||||||
|
: String(value);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isAxiosError(source)) {
|
||||||
|
return {
|
||||||
|
httpStatus: source.response?.status,
|
||||||
|
requestHeaders: toPlainHeaders(
|
||||||
|
overrideRequestHeaders ??
|
||||||
|
(source.config?.headers as Record<string, unknown> | undefined),
|
||||||
|
),
|
||||||
|
responseHeaders: toPlainHeaders(
|
||||||
|
source.response?.headers as Record<string, unknown> | undefined,
|
||||||
|
),
|
||||||
|
responseBody: source.response?.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = source as AxiosResponse | null;
|
||||||
|
if (
|
||||||
|
response &&
|
||||||
|
typeof response === "object" &&
|
||||||
|
"status" in response &&
|
||||||
|
"headers" in response
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
httpStatus: response.status,
|
||||||
|
requestHeaders: toPlainHeaders(
|
||||||
|
overrideRequestHeaders ??
|
||||||
|
(response.config?.headers as Record<string, unknown> | undefined),
|
||||||
|
),
|
||||||
|
responseHeaders: toPlainHeaders(
|
||||||
|
response.headers as Record<string, unknown> | undefined,
|
||||||
|
),
|
||||||
|
responseBody: response.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestHeaders: toPlainHeaders(overrideRequestHeaders),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
sanitizeErrorDetails(error: unknown): Record<string, unknown> {
|
sanitizeErrorDetails(error: unknown): Record<string, unknown> {
|
||||||
if (isAxiosError(error)) {
|
if (isAxiosError(error)) {
|
||||||
const data = error.response?.data;
|
const data = error.response?.data;
|
||||||
@@ -54,12 +222,13 @@ export class FanavaranAuditService {
|
|||||||
type: "axios",
|
type: "axios",
|
||||||
status: error.response?.status,
|
status: error.response?.status,
|
||||||
statusText: error.response?.statusText,
|
statusText: error.response?.statusText,
|
||||||
data:
|
data: this.sanitizeBody(
|
||||||
typeof data === "object" && data !== null
|
typeof data === "object" && data !== null
|
||||||
? data
|
? data
|
||||||
: typeof data === "string"
|
: typeof data === "string"
|
||||||
? data.slice(0, 2000)
|
? data.slice(0, 2000)
|
||||||
: data,
|
: data,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@@ -112,8 +281,13 @@ export class FanavaranAuditService {
|
|||||||
? { claimRequestId: new Types.ObjectId(input.session.claimRequestId) }
|
? { claimRequestId: new Types.ObjectId(input.session.claimRequestId) }
|
||||||
: {}),
|
: {}),
|
||||||
requestUrl: input.requestUrl,
|
requestUrl: input.requestUrl,
|
||||||
|
requestMethod: input.requestMethod,
|
||||||
httpStatus: input.httpStatus,
|
httpStatus: input.httpStatus,
|
||||||
|
requestHeaders: this.sanitizeHeaders(input.requestHeaders),
|
||||||
|
requestBody: this.sanitizeBody(input.requestBody),
|
||||||
requestMeta: input.requestMeta,
|
requestMeta: input.requestMeta,
|
||||||
|
responseHeaders: this.sanitizeHeaders(input.responseHeaders),
|
||||||
|
responseBody: this.sanitizeBody(input.responseBody),
|
||||||
responseMeta: input.responseMeta,
|
responseMeta: input.responseMeta,
|
||||||
errorMessage: input.errorMessage,
|
errorMessage: input.errorMessage,
|
||||||
errorDetails: input.errorDetails,
|
errorDetails: input.errorDetails,
|
||||||
@@ -134,4 +308,31 @@ export class FanavaranAuditService {
|
|||||||
.lean()
|
.lean()
|
||||||
.exec();
|
.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private maskSensitiveDeep(value: unknown, depth = 0): unknown {
|
||||||
|
if (depth > 8) {
|
||||||
|
return "[max-depth]";
|
||||||
|
}
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => this.maskSensitiveDeep(item, depth + 1));
|
||||||
|
}
|
||||||
|
if (typeof value === "object") {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [key, child] of Object.entries(
|
||||||
|
value as Record<string, unknown>,
|
||||||
|
)) {
|
||||||
|
if (FanavaranAuditService.SENSITIVE_BODY_KEYS.has(key.toLowerCase())) {
|
||||||
|
out[key] =
|
||||||
|
typeof child === "string" ? this.maskToken(child) : "[redacted]";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out[key] = this.maskSensitiveDeep(child, depth + 1);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,43 @@
|
|||||||
import { FanavaranAuthService } from "./fanavaran-auth.service";
|
import { FanavaranAuthService } from "./fanavaran-auth.service";
|
||||||
|
|
||||||
describe("FanavaranAuthService", () => {
|
describe("FanavaranAuthService", () => {
|
||||||
|
const createAuthTokenModel = () => {
|
||||||
|
const store = new Map<
|
||||||
|
string,
|
||||||
|
{ clientKey: string; authenticationToken: string; expiresAt: Date }
|
||||||
|
>();
|
||||||
|
return {
|
||||||
|
findOne: jest.fn((query: { clientKey: string }) => ({
|
||||||
|
lean: () => ({
|
||||||
|
exec: async () => store.get(query.clientKey) ?? null,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
findOneAndUpdate: jest.fn(
|
||||||
|
(
|
||||||
|
query: { clientKey: string },
|
||||||
|
update: { $set: { authenticationToken: string; expiresAt: Date } },
|
||||||
|
) => ({
|
||||||
|
exec: async () => {
|
||||||
|
const next = {
|
||||||
|
clientKey: query.clientKey,
|
||||||
|
authenticationToken: update.$set.authenticationToken,
|
||||||
|
expiresAt: update.$set.expiresAt,
|
||||||
|
};
|
||||||
|
store.set(query.clientKey, next);
|
||||||
|
return next;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
deleteOne: jest.fn((query: { clientKey: string }) => ({
|
||||||
|
exec: async () => {
|
||||||
|
store.delete(query.clientKey);
|
||||||
|
return { deletedCount: 1 };
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
_store: store,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
it("detects Fanavaran transient try-later messages", () => {
|
it("detects Fanavaran transient try-later messages", () => {
|
||||||
expect(
|
expect(
|
||||||
FanavaranAuthService.isTransientTryLaterError(
|
FanavaranAuthService.isTransientTryLaterError(
|
||||||
@@ -24,22 +61,11 @@ describe("FanavaranAuthService", () => {
|
|||||||
e instanceof Error ? e.message : String(e),
|
e instanceof Error ? e.message : String(e),
|
||||||
sanitizeErrorDetails: () => ({}),
|
sanitizeErrorDetails: () => ({}),
|
||||||
formatErrorWithTrackingCode: (m: string) => m,
|
formatErrorWithTrackingCode: (m: string) => m,
|
||||||
|
captureAxiosExchange: () => ({}),
|
||||||
};
|
};
|
||||||
|
const authTokenModel = createAuthTokenModel();
|
||||||
|
|
||||||
let loginCalls = 0;
|
let loginCalls = 0;
|
||||||
http.post.mockImplementation((url: string) => {
|
|
||||||
if (url.includes("GetAppToken")) {
|
|
||||||
return {
|
|
||||||
toPromise: undefined,
|
|
||||||
pipe: undefined,
|
|
||||||
subscribe: undefined,
|
|
||||||
// firstValueFrom uses Observable — mock as Observable-like via rxjs
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use real firstValueFrom path by mocking httpService.post to return an Observable
|
|
||||||
const { of, delay } = await import("rxjs");
|
const { of, delay } = await import("rxjs");
|
||||||
http.post.mockImplementation((url: string) => {
|
http.post.mockImplementation((url: string) => {
|
||||||
if (url.includes("GetAppToken")) {
|
if (url.includes("GetAppToken")) {
|
||||||
@@ -57,7 +83,11 @@ describe("FanavaranAuthService", () => {
|
|||||||
}).pipe(delay(20));
|
}).pipe(delay(20));
|
||||||
});
|
});
|
||||||
|
|
||||||
const service = new FanavaranAuthService(http as any, audit as any);
|
const service = new FanavaranAuthService(
|
||||||
|
http as any,
|
||||||
|
audit as any,
|
||||||
|
authTokenModel as any,
|
||||||
|
);
|
||||||
|
|
||||||
const [a, b, c] = await Promise.all([
|
const [a, b, c] = await Promise.all([
|
||||||
service.getAuthenticationToken("parsian"),
|
service.getAuthenticationToken("parsian"),
|
||||||
@@ -75,28 +105,74 @@ describe("FanavaranAuthService", () => {
|
|||||||
expect(loginCalls).toBe(1);
|
expect(loginCalls).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reuses persisted token across service instances", async () => {
|
||||||
|
const http = { post: jest.fn() };
|
||||||
|
const audit = {
|
||||||
|
recordStep: jest.fn().mockResolvedValue(undefined),
|
||||||
|
extractErrorMessage: (e: unknown) =>
|
||||||
|
e instanceof Error ? e.message : String(e),
|
||||||
|
sanitizeErrorDetails: () => ({}),
|
||||||
|
formatErrorWithTrackingCode: (m: string) => m,
|
||||||
|
captureAxiosExchange: () => ({}),
|
||||||
|
};
|
||||||
|
const authTokenModel = createAuthTokenModel();
|
||||||
|
const { of } = await import("rxjs");
|
||||||
|
let loginCalls = 0;
|
||||||
|
http.post.mockImplementation((url: string) => {
|
||||||
|
if (url.includes("GetAppToken")) {
|
||||||
|
return of({
|
||||||
|
status: 200,
|
||||||
|
headers: { apptoken: "app-1" },
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loginCalls += 1;
|
||||||
|
return of({
|
||||||
|
status: 200,
|
||||||
|
headers: { authenticationtoken: "auth-persisted" },
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = new FanavaranAuthService(
|
||||||
|
http as any,
|
||||||
|
audit as any,
|
||||||
|
authTokenModel as any,
|
||||||
|
);
|
||||||
|
await first.getAuthenticationToken("tejaratno");
|
||||||
|
expect(loginCalls).toBe(1);
|
||||||
|
|
||||||
|
const second = new FanavaranAuthService(
|
||||||
|
http as any,
|
||||||
|
audit as any,
|
||||||
|
authTokenModel as any,
|
||||||
|
);
|
||||||
|
const token = await second.getAuthenticationToken("tejaratno");
|
||||||
|
expect(token).toBe("auth-persisted");
|
||||||
|
expect(loginCalls).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("enters tenant backoff on try-later errors", () => {
|
it("enters tenant backoff on try-later errors", () => {
|
||||||
const service = new FanavaranAuthService({} as any, {} as any);
|
const service = new FanavaranAuthService(
|
||||||
|
{} as any,
|
||||||
|
{} as any,
|
||||||
|
createAuthTokenModel() as any,
|
||||||
|
);
|
||||||
service.registerFailure(
|
service.registerFailure(
|
||||||
"parsian",
|
"parsian",
|
||||||
"کد پیگیری خطا: 1\r\n.لطفا پس از چند لحظه مجدد تلاش فرمایید.",
|
"کد پیگیری خطا: 1\r\n.لطفا پس از چند لحظه مجدد تلاش فرمایید.",
|
||||||
);
|
);
|
||||||
expect(service.isInBackoff("parsian")).toBe(true);
|
expect(service.isInBackoff("parsian")).toBe(true);
|
||||||
expect(() => service.assertNotInBackoff("parsian")).toThrow(
|
expect(() => service.assertNotInBackoff("parsian")).toThrow(/backoff/i);
|
||||||
/backoff/i,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("computes next Asia/Tehran midnight expiry after now", () => {
|
it("computes next Asia/Tehran midnight expiry after now", () => {
|
||||||
// 2026-08-02 10:00:00 UTC ≈ 13:30 Tehran (UTC+3:30) → same calendar day midnight
|
|
||||||
const now = Date.parse("2026-08-02T10:00:00.000Z");
|
const now = Date.parse("2026-08-02T10:00:00.000Z");
|
||||||
const expiry = FanavaranAuthService.getNextMidnightExpiryMs(now);
|
const expiry = FanavaranAuthService.getNextMidnightExpiryMs(now);
|
||||||
expect(expiry).toBeGreaterThan(now);
|
expect(expiry).toBeGreaterThan(now);
|
||||||
// Must land within ~14h (before next Tehran midnight)
|
|
||||||
expect(expiry - now).toBeLessThanOrEqual(24 * 60 * 60 * 1000);
|
expect(expiry - now).toBeLessThanOrEqual(24 * 60 * 60 * 1000);
|
||||||
expect(expiry - now).toBeGreaterThan(0);
|
expect(expiry - now).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Just after Tehran midnight: 2026-08-01 20:30:01 UTC = 2026-08-02 00:00:01 Tehran
|
|
||||||
const justAfterMidnight = Date.parse("2026-08-01T20:30:01.000Z");
|
const justAfterMidnight = Date.parse("2026-08-01T20:30:01.000Z");
|
||||||
const next = FanavaranAuthService.getNextMidnightExpiryMs(justAfterMidnight);
|
const next = FanavaranAuthService.getNextMidnightExpiryMs(justAfterMidnight);
|
||||||
expect(next - justAfterMidnight).toBeGreaterThan(23 * 60 * 60 * 1000);
|
expect(next - justAfterMidnight).toBeGreaterThan(23 * 60 * 60 * 1000);
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
ServiceUnavailableException,
|
ServiceUnavailableException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
|
import { InjectModel } from "@nestjs/mongoose";
|
||||||
import { isAxiosError } from "axios";
|
import { isAxiosError } from "axios";
|
||||||
|
import { Model } from "mongoose";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
import {
|
import {
|
||||||
getFanavaranClientProfile,
|
getFanavaranClientProfile,
|
||||||
@@ -19,6 +21,10 @@ import {
|
|||||||
FanavaranAuditStatus,
|
FanavaranAuditStatus,
|
||||||
FanavaranAuditStep,
|
FanavaranAuditStep,
|
||||||
} from "./schema/fanavaran-audit-log.schema";
|
} from "./schema/fanavaran-audit-log.schema";
|
||||||
|
import {
|
||||||
|
FanavaranAuthToken,
|
||||||
|
FanavaranAuthTokenDocument,
|
||||||
|
} from "./schema/fanavaran-auth-token.schema";
|
||||||
|
|
||||||
interface CachedFanavaranAuth {
|
interface CachedFanavaranAuth {
|
||||||
authenticationToken: string;
|
authenticationToken: string;
|
||||||
@@ -63,6 +69,8 @@ export class FanavaranAuthService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly fanavaranAuditService: FanavaranAuditService,
|
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). */
|
/** 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 {
|
invalidateToken(clientKey: FanavaranClientKey): void {
|
||||||
this.tokenCache.delete(clientKey);
|
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 {
|
assertNotInBackoff(clientKey: FanavaranClientKey): void {
|
||||||
@@ -183,9 +197,13 @@ export class FanavaranAuthService {
|
|||||||
this.assertNotInBackoff(clientKey);
|
this.assertNotInBackoff(clientKey);
|
||||||
|
|
||||||
if (!options?.forceRefresh) {
|
if (!options?.forceRefresh) {
|
||||||
const cached = this.tokenCache.get(clientKey);
|
const memoryHit = this.readMemoryCache(clientKey);
|
||||||
if (cached && cached.expiresAt > Date.now()) {
|
if (memoryHit) {
|
||||||
return cached.authenticationToken;
|
return memoryHit;
|
||||||
|
}
|
||||||
|
const persisted = await this.readPersistedCache(clientKey);
|
||||||
|
if (persisted) {
|
||||||
|
return persisted;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.invalidateToken(clientKey);
|
this.invalidateToken(clientKey);
|
||||||
@@ -243,10 +261,7 @@ export class FanavaranAuthService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const expiresAt = FanavaranAuthService.getNextMidnightExpiryMs();
|
const expiresAt = FanavaranAuthService.getNextMidnightExpiryMs();
|
||||||
this.tokenCache.set(clientKey, {
|
await this.persistToken(clientKey, authenticationToken, expiresAt);
|
||||||
authenticationToken,
|
|
||||||
expiresAt,
|
|
||||||
});
|
|
||||||
this.clearBackoff(clientKey);
|
this.clearBackoff(clientKey);
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`[${clientKey}] Cached Fanavaran authenticationToken until ${new Date(
|
`[${clientKey}] Cached Fanavaran authenticationToken until ${new Date(
|
||||||
@@ -256,6 +271,76 @@ export class FanavaranAuthService {
|
|||||||
return authenticationToken;
|
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() {
|
private emptyBodyTransformRequest() {
|
||||||
return [
|
return [
|
||||||
(_data: unknown, headers?: Record<string, unknown>) => {
|
(_data: unknown, headers?: Record<string, unknown>) => {
|
||||||
@@ -273,12 +358,20 @@ export class FanavaranAuthService {
|
|||||||
auditSession?: FanavaranAuditSession,
|
auditSession?: FanavaranAuditSession,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
const requestHeaders = {
|
||||||
|
appname: config.appName,
|
||||||
|
secret: config.secret,
|
||||||
|
"Content-Length": "0",
|
||||||
|
};
|
||||||
if (auditSession) {
|
if (auditSession) {
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.GET_APP_TOKEN,
|
step: FanavaranAuditStep.GET_APP_TOKEN,
|
||||||
status: FanavaranAuditStatus.STARTED,
|
status: FanavaranAuditStatus.STARTED,
|
||||||
requestUrl: this.getAppTokenUrl,
|
requestUrl: this.getAppTokenUrl,
|
||||||
|
requestMethod: "POST",
|
||||||
|
requestHeaders,
|
||||||
|
requestBody: "",
|
||||||
requestMeta: { appName: config.appName, cached: false },
|
requestMeta: { appName: config.appName, cached: false },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -286,11 +379,7 @@ export class FanavaranAuthService {
|
|||||||
try {
|
try {
|
||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
this.httpService.post(this.getAppTokenUrl, "", {
|
this.httpService.post(this.getAppTokenUrl, "", {
|
||||||
headers: {
|
headers: requestHeaders,
|
||||||
appname: config.appName,
|
|
||||||
secret: config.secret,
|
|
||||||
"Content-Length": "0",
|
|
||||||
},
|
|
||||||
transformRequest: this.emptyBodyTransformRequest(),
|
transformRequest: this.emptyBodyTransformRequest(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -306,13 +395,23 @@ export class FanavaranAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (auditSession) {
|
if (auditSession) {
|
||||||
|
const exchange =
|
||||||
|
this.fanavaranAuditService.captureAxiosExchange(
|
||||||
|
response,
|
||||||
|
requestHeaders,
|
||||||
|
);
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.GET_APP_TOKEN,
|
step: FanavaranAuditStep.GET_APP_TOKEN,
|
||||||
status: FanavaranAuditStatus.SUCCESS,
|
status: FanavaranAuditStatus.SUCCESS,
|
||||||
requestUrl: this.getAppTokenUrl,
|
requestUrl: this.getAppTokenUrl,
|
||||||
|
requestMethod: "POST",
|
||||||
httpStatus: response.status,
|
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,
|
durationMs: Date.now() - startedAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -320,12 +419,21 @@ export class FanavaranAuthService {
|
|||||||
return appToken;
|
return appToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (auditSession) {
|
if (auditSession) {
|
||||||
|
const exchange = this.fanavaranAuditService.captureAxiosExchange(
|
||||||
|
error,
|
||||||
|
requestHeaders,
|
||||||
|
);
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.GET_APP_TOKEN,
|
step: FanavaranAuditStep.GET_APP_TOKEN,
|
||||||
status: FanavaranAuditStatus.FAILURE,
|
status: FanavaranAuditStatus.FAILURE,
|
||||||
requestUrl: this.getAppTokenUrl,
|
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),
|
errorMessage: this.fanavaranAuditService.extractErrorMessage(error),
|
||||||
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
@@ -341,12 +449,21 @@ export class FanavaranAuthService {
|
|||||||
auditSession?: FanavaranAuditSession,
|
auditSession?: FanavaranAuditSession,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
|
const requestHeaders = {
|
||||||
|
appToken,
|
||||||
|
userName: config.username,
|
||||||
|
password: config.password,
|
||||||
|
"Content-Length": "0",
|
||||||
|
};
|
||||||
if (auditSession) {
|
if (auditSession) {
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.LOGIN,
|
step: FanavaranAuditStep.LOGIN,
|
||||||
status: FanavaranAuditStatus.STARTED,
|
status: FanavaranAuditStatus.STARTED,
|
||||||
requestUrl: this.loginUrl,
|
requestUrl: this.loginUrl,
|
||||||
|
requestMethod: "POST",
|
||||||
|
requestHeaders,
|
||||||
|
requestBody: "",
|
||||||
requestMeta: { userName: config.username, cached: false },
|
requestMeta: { userName: config.username, cached: false },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -354,12 +471,7 @@ export class FanavaranAuthService {
|
|||||||
try {
|
try {
|
||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
this.httpService.post(this.loginUrl, "", {
|
this.httpService.post(this.loginUrl, "", {
|
||||||
headers: {
|
headers: requestHeaders,
|
||||||
appToken,
|
|
||||||
userName: config.username,
|
|
||||||
password: config.password,
|
|
||||||
"Content-Length": "0",
|
|
||||||
},
|
|
||||||
transformRequest: this.emptyBodyTransformRequest(),
|
transformRequest: this.emptyBodyTransformRequest(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -380,13 +492,26 @@ export class FanavaranAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (auditSession) {
|
if (auditSession) {
|
||||||
|
const exchange =
|
||||||
|
this.fanavaranAuditService.captureAxiosExchange(
|
||||||
|
response,
|
||||||
|
requestHeaders,
|
||||||
|
);
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.LOGIN,
|
step: FanavaranAuditStep.LOGIN,
|
||||||
status: FanavaranAuditStatus.SUCCESS,
|
status: FanavaranAuditStatus.SUCCESS,
|
||||||
requestUrl: this.loginUrl,
|
requestUrl: this.loginUrl,
|
||||||
|
requestMethod: "POST",
|
||||||
httpStatus: response.status,
|
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,
|
durationMs: Date.now() - startedAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -394,12 +519,21 @@ export class FanavaranAuthService {
|
|||||||
return authenticationToken;
|
return authenticationToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (auditSession) {
|
if (auditSession) {
|
||||||
|
const exchange = this.fanavaranAuditService.captureAxiosExchange(
|
||||||
|
error,
|
||||||
|
requestHeaders,
|
||||||
|
);
|
||||||
await this.fanavaranAuditService.recordStep({
|
await this.fanavaranAuditService.recordStep({
|
||||||
session: auditSession,
|
session: auditSession,
|
||||||
step: FanavaranAuditStep.LOGIN,
|
step: FanavaranAuditStep.LOGIN,
|
||||||
status: FanavaranAuditStatus.FAILURE,
|
status: FanavaranAuditStatus.FAILURE,
|
||||||
requestUrl: this.loginUrl,
|
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),
|
errorMessage: this.fanavaranAuditService.extractErrorMessage(error),
|
||||||
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
errorDetails: this.fanavaranAuditService.sanitizeErrorDetails(error),
|
||||||
durationMs: Date.now() - startedAt,
|
durationMs: Date.now() - startedAt,
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { HttpModule } from "@nestjs/axios";
|
import { HttpModule } from "@nestjs/axios";
|
||||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||||
|
import { MongooseModule } from "@nestjs/mongoose";
|
||||||
import { createHttpModuleOptions } from "src/core/config/http-proxy.factory";
|
import { createHttpModuleOptions } from "src/core/config/http-proxy.factory";
|
||||||
import { FanavaranAuditModule } from "./fanavaran-audit.module";
|
import { FanavaranAuditModule } from "./fanavaran-audit.module";
|
||||||
import { FanavaranAuthService } from "./fanavaran-auth.service";
|
import { FanavaranAuthService } from "./fanavaran-auth.service";
|
||||||
import { FanavaranLookupService } from "./fanavaran-lookup.service";
|
import { FanavaranLookupService } from "./fanavaran-lookup.service";
|
||||||
|
import {
|
||||||
|
FanavaranAuthToken,
|
||||||
|
FanavaranAuthTokenSchema,
|
||||||
|
} from "./schema/fanavaran-auth-token.schema";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -13,6 +18,9 @@ import { FanavaranLookupService } from "./fanavaran-lookup.service";
|
|||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: createHttpModuleOptions,
|
useFactory: createHttpModuleOptions,
|
||||||
}),
|
}),
|
||||||
|
MongooseModule.forFeature([
|
||||||
|
{ name: FanavaranAuthToken.name, schema: FanavaranAuthTokenSchema },
|
||||||
|
]),
|
||||||
FanavaranAuditModule,
|
FanavaranAuditModule,
|
||||||
],
|
],
|
||||||
providers: [FanavaranAuthService, FanavaranLookupService],
|
providers: [FanavaranAuthService, FanavaranLookupService],
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
||||||
import {
|
import {
|
||||||
|
FANAVARAN_CLIENT_KEYS,
|
||||||
|
FANAVARAN_CLIENT_SWAGGER_ENUM,
|
||||||
isFanavaranClientKey,
|
isFanavaranClientKey,
|
||||||
listFanavaranClientProfiles,
|
listFanavaranClientProfiles,
|
||||||
normalizeFanavaranClientKey,
|
normalizeFanavaranClientKey,
|
||||||
@@ -37,7 +39,7 @@ export class FanavaranController {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "List supported Fanavaran insurance clients",
|
summary: "List supported Fanavaran insurance clients",
|
||||||
description:
|
description:
|
||||||
"Returns configured Fanavaran tenants (parsian, tejaratno) and which client is active for this deployment.",
|
"Returns configured Fanavaran tenants (parsian, tejaratno, moallem) and which client is active for this deployment.",
|
||||||
})
|
})
|
||||||
listClients() {
|
listClients() {
|
||||||
const activeClient = resolveFanavaranClientKey();
|
const activeClient = resolveFanavaranClientKey();
|
||||||
@@ -60,7 +62,7 @@ export class FanavaranController {
|
|||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "client",
|
name: "client",
|
||||||
description: "Fanavaran tenant key",
|
description: "Fanavaran tenant key",
|
||||||
enum: ["parsian", "tejaratno"],
|
enum: FANAVARAN_CLIENT_SWAGGER_ENUM,
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "claimCaseId",
|
name: "claimCaseId",
|
||||||
@@ -75,26 +77,33 @@ export class FanavaranController {
|
|||||||
name: "forceRefreshPolicy",
|
name: "forceRefreshPolicy",
|
||||||
required: false,
|
required: false,
|
||||||
description:
|
description:
|
||||||
"When true, ignores cached PolicyId and performs a live Fanavaran policy inquiry again",
|
"When true, ignores cached PolicyId and performs a live Fanavaran policy inquiry again. Do not pass this from normal UI loads.",
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "resolvePolicy",
|
||||||
|
required: false,
|
||||||
|
deprecated: true,
|
||||||
|
description:
|
||||||
|
"Deprecated. Ignored for cache-busting. PolicyId is resolve-once from fanavaranSync.baseClaim.policyId; use forceRefreshPolicy=true only to re-inquire.",
|
||||||
})
|
})
|
||||||
async preview(
|
async preview(
|
||||||
@Param("client") client: string,
|
@Param("client") client: string,
|
||||||
@Param("claimCaseId") claimCaseId: string,
|
@Param("claimCaseId") claimCaseId: string,
|
||||||
@Query("debug") debug?: string,
|
@Query("debug") debug?: string,
|
||||||
@Query("forceRefreshPolicy") forceRefreshPolicy?: string,
|
@Query("forceRefreshPolicy") forceRefreshPolicy?: string,
|
||||||
@Query("resolvePolicy") resolvePolicy?: string,
|
@Query("resolvePolicy") _resolvePolicy?: string,
|
||||||
) {
|
) {
|
||||||
const clientKey = this.parseClientParam(client);
|
const clientKey = this.parseClientParam(client);
|
||||||
|
// IMPORTANT: resolvePolicy must NOT force a live inquiry. Older UI clients
|
||||||
|
// send resolvePolicy=true on every preview load; that used to defeat the
|
||||||
|
// PolicyId cache and re-Login Fanavaran on every click.
|
||||||
return await this.claimRequestManagementService.previewFanavaranSubmitV2(
|
return await this.claimRequestManagementService.previewFanavaranSubmitV2(
|
||||||
claimCaseId,
|
claimCaseId,
|
||||||
clientKey,
|
clientKey,
|
||||||
{
|
{
|
||||||
debug: debug === "1" || debug === "true",
|
debug: debug === "1" || debug === "true",
|
||||||
forceRefreshPolicy:
|
forceRefreshPolicy:
|
||||||
forceRefreshPolicy === "1" ||
|
forceRefreshPolicy === "1" || forceRefreshPolicy === "true",
|
||||||
forceRefreshPolicy === "true" ||
|
|
||||||
resolvePolicy === "1" ||
|
|
||||||
resolvePolicy === "true",
|
|
||||||
requirePolicyId: false,
|
requirePolicyId: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -109,7 +118,7 @@ export class FanavaranController {
|
|||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "client",
|
name: "client",
|
||||||
description: "Fanavaran tenant key",
|
description: "Fanavaran tenant key",
|
||||||
enum: ["parsian", "tejaratno"],
|
enum: FANAVARAN_CLIENT_SWAGGER_ENUM,
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "claimCaseId",
|
name: "claimCaseId",
|
||||||
@@ -137,7 +146,7 @@ export class FanavaranController {
|
|||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "client",
|
name: "client",
|
||||||
description: "Fanavaran tenant key",
|
description: "Fanavaran tenant key",
|
||||||
enum: ["parsian", "tejaratno"],
|
enum: FANAVARAN_CLIENT_SWAGGER_ENUM,
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "claimCaseId",
|
name: "claimCaseId",
|
||||||
@@ -158,12 +167,12 @@ export class FanavaranController {
|
|||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Submit Fanavaran damage-case request",
|
summary: "Submit Fanavaran damage-case request",
|
||||||
description:
|
description:
|
||||||
"Submits the GEN.12 dmg-cases request for the already-created Fanavaran claim and stores returned Id as local dmgCaseId.",
|
"Submits the GEN.12 dmg-cases request. If base claim (claimId) is missing, soft-ensures GEN.03 base claim first, then submits damage. Skips when dmgCaseId already exists.",
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "client",
|
name: "client",
|
||||||
description: "Fanavaran tenant key",
|
description: "Fanavaran tenant key",
|
||||||
enum: ["parsian", "tejaratno"],
|
enum: FANAVARAN_CLIENT_SWAGGER_ENUM,
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "claimCaseId",
|
name: "claimCaseId",
|
||||||
@@ -191,7 +200,7 @@ export class FanavaranController {
|
|||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "client",
|
name: "client",
|
||||||
description: "Fanavaran tenant key",
|
description: "Fanavaran tenant key",
|
||||||
enum: ["parsian", "tejaratno"],
|
enum: FANAVARAN_CLIENT_SWAGGER_ENUM,
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "claimCaseId",
|
name: "claimCaseId",
|
||||||
@@ -217,7 +226,7 @@ export class FanavaranController {
|
|||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "client",
|
name: "client",
|
||||||
description: "Fanavaran tenant key",
|
description: "Fanavaran tenant key",
|
||||||
enum: ["parsian", "tejaratno"],
|
enum: FANAVARAN_CLIENT_SWAGGER_ENUM,
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "claimCaseId",
|
name: "claimCaseId",
|
||||||
@@ -243,7 +252,7 @@ export class FanavaranController {
|
|||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "client",
|
name: "client",
|
||||||
description: "Fanavaran tenant key",
|
description: "Fanavaran tenant key",
|
||||||
enum: ["parsian", "tejaratno"],
|
enum: FANAVARAN_CLIENT_SWAGGER_ENUM,
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "claimCaseId",
|
name: "claimCaseId",
|
||||||
@@ -269,7 +278,7 @@ export class FanavaranController {
|
|||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "client",
|
name: "client",
|
||||||
description: "Fanavaran tenant key",
|
description: "Fanavaran tenant key",
|
||||||
enum: ["parsian", "tejaratno"],
|
enum: FANAVARAN_CLIENT_SWAGGER_ENUM,
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: "claimCaseId",
|
name: "claimCaseId",
|
||||||
@@ -291,7 +300,7 @@ export class FanavaranController {
|
|||||||
private parseClientParam(client: string) {
|
private parseClientParam(client: string) {
|
||||||
if (!isFanavaranClientKey(client)) {
|
if (!isFanavaranClientKey(client)) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`Invalid Fanavaran client "${client}". Expected one of: parsian, tejaratno`,
|
`Invalid Fanavaran client "${client}". Expected one of: ${FANAVARAN_CLIENT_KEYS.join(", ")}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return normalizeFanavaranClientKey(client);
|
return normalizeFanavaranClientKey(client);
|
||||||
|
|||||||
@@ -62,12 +62,33 @@ export class FanavaranAuditLog {
|
|||||||
@Prop({ type: String, required: false })
|
@Prop({ type: String, required: false })
|
||||||
requestUrl?: string;
|
requestUrl?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String, required: false })
|
||||||
|
requestMethod?: string;
|
||||||
|
|
||||||
@Prop({ type: Number, required: false })
|
@Prop({ type: Number, required: false })
|
||||||
httpStatus?: number;
|
httpStatus?: number;
|
||||||
|
|
||||||
|
/** Sanitized outbound headers (secrets masked). */
|
||||||
|
@Prop({ type: Object, required: false })
|
||||||
|
requestHeaders?: Record<string, unknown>;
|
||||||
|
|
||||||
|
/** Outbound JSON/body (truncated; secrets masked). */
|
||||||
|
@Prop({ type: Object, required: false })
|
||||||
|
requestBody?: unknown;
|
||||||
|
|
||||||
|
/** Compact structured facts (ids, flags) — kept for filtering/dashboards. */
|
||||||
@Prop({ type: Object, required: false })
|
@Prop({ type: Object, required: false })
|
||||||
requestMeta?: Record<string, unknown>;
|
requestMeta?: Record<string, unknown>;
|
||||||
|
|
||||||
|
/** Sanitized inbound response headers. */
|
||||||
|
@Prop({ type: Object, required: false })
|
||||||
|
responseHeaders?: Record<string, unknown>;
|
||||||
|
|
||||||
|
/** Inbound response body (truncated). */
|
||||||
|
@Prop({ type: Object, required: false })
|
||||||
|
responseBody?: unknown;
|
||||||
|
|
||||||
|
/** Compact structured facts from the response. */
|
||||||
@Prop({ type: Object, required: false })
|
@Prop({ type: Object, required: false })
|
||||||
responseMeta?: Record<string, unknown>;
|
responseMeta?: Record<string, unknown>;
|
||||||
|
|
||||||
@@ -86,3 +107,4 @@ export const FanavaranAuditLogSchema =
|
|||||||
SchemaFactory.createForClass(FanavaranAuditLog);
|
SchemaFactory.createForClass(FanavaranAuditLog);
|
||||||
|
|
||||||
FanavaranAuditLogSchema.index({ trackingCode: 1, createdAt: 1 });
|
FanavaranAuditLogSchema.index({ trackingCode: 1, createdAt: 1 });
|
||||||
|
FanavaranAuditLogSchema.index({ claimCaseId: 1, createdAt: 1 });
|
||||||
|
|||||||
24
src/fanavaran/schema/fanavaran-auth-token.schema.ts
Normal file
24
src/fanavaran/schema/fanavaran-auth-token.schema.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { HydratedDocument } from "mongoose";
|
||||||
|
import type { FanavaranClientKey } from "src/core/config/fanavaran-client.config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared Fanavaran authenticationToken per tenant.
|
||||||
|
* Survives process restarts / multi-instance so we do not Login on every request.
|
||||||
|
*/
|
||||||
|
@Schema({ collection: "fanavaranAuthTokens", timestamps: true })
|
||||||
|
export class FanavaranAuthToken {
|
||||||
|
@Prop({ type: String, required: true, unique: true, index: true })
|
||||||
|
clientKey: FanavaranClientKey;
|
||||||
|
|
||||||
|
@Prop({ type: String, required: true })
|
||||||
|
authenticationToken: string;
|
||||||
|
|
||||||
|
/** When this token should be refreshed (Asia/Tehran midnight). */
|
||||||
|
@Prop({ type: Date, required: true, index: true })
|
||||||
|
expiresAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FanavaranAuthTokenDocument = HydratedDocument<FanavaranAuthToken>;
|
||||||
|
export const FanavaranAuthTokenSchema =
|
||||||
|
SchemaFactory.createForClass(FanavaranAuthToken);
|
||||||
Reference in New Issue
Block a user