From 8d396762a22b31cd27f01459debe6656e397f3db Mon Sep 17 00:00:00 2001 From: "s.hajizadeh" Date: Tue, 30 Jun 2026 11:51:11 +0330 Subject: [PATCH] fix(fanavaran): select latest active policy by end date Why: - Fanavaran policy inquiry response order is inconsistent, so selecting the last item can choose an old or expired policy. Changes: - Select the policy with the latest Jalali EndDate. - Reject empty policy responses, expired latest policies, and latest policies without a valid PolicyId. - Stop Fanavaran submission when PolicyId cannot be resolved. Impact: - Fanavaran submit now fails clearly instead of continuing with PolicyId: null. --- .../claim-request-management.service.ts | 84 ++++++---------- .../fanavaran-policy-selection.spec.ts | 63 ++++++++++++ .../fanavaran-policy-selection.ts | 95 +++++++++++++++++++ 3 files changed, 187 insertions(+), 55 deletions(-) create mode 100644 src/claim-request-management/fanavaran-policy-selection.spec.ts create mode 100644 src/claim-request-management/fanavaran-policy-selection.ts diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index b86ce03..e971684 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -176,6 +176,7 @@ import { FanavaranAuditStatus, FanavaranAuditStep, } from "src/fanavaran/schema/fanavaran-audit-log.schema"; +import { selectLatestActiveFanavaranPolicy } from "./fanavaran-policy-selection"; export interface FanavaranAutoSubmitResult { attempted: boolean; @@ -3438,17 +3439,13 @@ export class ClaimRequestManagementService { this.applyFanavaranDefaultFields(result); - try { - const policyId = await input.resolvePolicyId(); - if (policyId !== undefined && policyId !== null) { - result.PolicyId = policyId; - } - } catch (error) { - this.logger.error( - `${input.logPrefix} Failed to get PolicyId from external API:`, - error, + const policyId = await input.resolvePolicyId(); + if (policyId === undefined || policyId === null) { + throw new BadRequestException( + `${input.logPrefix} PolicyId is required for Fanavaran submit. Policy inquiry returned no valid policy; contact the administrator.`, ); } + result.PolicyId = policyId; return result; } @@ -3567,15 +3564,18 @@ export class ClaimRequestManagementService { blameRequest, claimRequestId, ); - if (policyId) { - result.PolicyId = policyId; + if (!policyId) { + throw new BadRequestException( + `[Fanavaran Submit] PolicyId is required for Fanavaran submit. Policy inquiry returned no valid policy; contact the administrator.`, + ); } + result.PolicyId = policyId; } catch (error) { this.logger.error( `[Fanavaran Submit] Failed to get PolicyId from external API:`, error, ); - // Continue without PolicyId if API call fails + throw error; } return result; @@ -3866,11 +3866,7 @@ export class ClaimRequestManagementService { }), ); - const policyCount = Array.isArray(response.data) - ? response.data.length - : 0; - const lastPolicy = - policyCount > 0 ? response.data[policyCount - 1] : null; + const policyCount = Array.isArray(response.data) ? response.data.length : 0; this.logger.log( `${logPrefix} Policy inquiry response status=${response.status} count=${policyCount}`, @@ -3882,39 +3878,11 @@ export class ClaimRequestManagementService { 2, )}`, ); - if (lastPolicy && typeof lastPolicy === "object") { - this.logger.log( - `${logPrefix} Policy inquiry last policy keys: ${Object.keys( - lastPolicy, - ).join(", ")}`, - ); - } + const selectedPolicy = selectLatestActiveFanavaranPolicy(response.data); + this.logger.log( + `${logPrefix} Selected latest active policy PolicyId=${selectedPolicy.policyId} EndDate=${selectedPolicy.endDate}`, + ); - if (Array.isArray(response.data) && response.data.length > 0) { - const policyId = lastPolicy?.PolicyId; - if (policyId !== undefined && policyId !== null) { - this.logger.log( - `${logPrefix} Successfully retrieved PolicyId from last policy entry: ${policyId}`, - ); - if (auditSession) { - await this.fanavaranAuditService.recordStep({ - session: auditSession, - step: FanavaranAuditStep.POLICY_INQUIRY, - status: FanavaranAuditStatus.SUCCESS, - requestUrl: policyInquiryUrl, - httpStatus: response.status, - responseMeta: { - policyId, - policyCount: response.data.length, - }, - durationMs: Date.now() - startedAt, - }); - } - return policyId; - } - } - - this.logger.warn(`${logPrefix} No PolicyId found in API response`); if (auditSession) { await this.fanavaranAuditService.recordStep({ session: auditSession, @@ -3923,15 +3891,15 @@ export class ClaimRequestManagementService { requestUrl: policyInquiryUrl, httpStatus: response.status, responseMeta: { - policyId: null, - policyCount: Array.isArray(response.data) - ? response.data.length - : 0, + policyId: selectedPolicy.policyId, + policyEndDate: selectedPolicy.endDate, + policyEndDateGregorian: selectedPolicy.endDateGregorian, + policyCount, }, durationMs: Date.now() - startedAt, }); } - return null; + return selectedPolicy.policyId; } catch (error) { const errorMessage = error instanceof Error ? error.message : "unknown policy inquiry error"; @@ -3948,8 +3916,11 @@ export class ClaimRequestManagementService { }); } this.logger.warn( - `${logPrefix} Policy inquiry failed; continuing with empty PolicyId: ${errorMessage}`, + `${logPrefix} Policy inquiry failed: ${errorMessage}`, ); + if (error instanceof BadRequestException) { + throw error; + } return null; } } @@ -4011,6 +3982,9 @@ export class ClaimRequestManagementService { `[Fanavaran Submit] Error getting PolicyId from external API:`, error, ); + if (error instanceof BadRequestException) { + throw error; + } return null; } } diff --git a/src/claim-request-management/fanavaran-policy-selection.spec.ts b/src/claim-request-management/fanavaran-policy-selection.spec.ts new file mode 100644 index 0000000..6759ab2 --- /dev/null +++ b/src/claim-request-management/fanavaran-policy-selection.spec.ts @@ -0,0 +1,63 @@ +import { BadRequestException } from "@nestjs/common"; +import { selectLatestActiveFanavaranPolicy } from "./fanavaran-policy-selection"; + +describe("selectLatestActiveFanavaranPolicy", () => { + const today = "2026-06-30"; + + it("selects the policy with the latest EndDate when newest is first", () => { + const selected = selectLatestActiveFanavaranPolicy( + [ + { PolicyId: 4826286, EndDate: "1405/09/23" }, + { PolicyId: 3719458, EndDate: "1404/09/06" }, + { PolicyId: 2800731, EndDate: "1403/09/05" }, + ], + today, + ); + + expect(selected.policyId).toBe(4826286); + expect(selected.endDate).toBe("1405/09/23"); + }); + + it("selects the policy with the latest EndDate when newest is last", () => { + const selected = selectLatestActiveFanavaranPolicy( + [ + { PolicyId: 2800731, EndDate: "1403/09/05" }, + { PolicyId: 3719458, EndDate: "1404/09/06" }, + { PolicyId: 4826286, EndDate: "1405/09/23" }, + ], + today, + ); + + expect(selected.policyId).toBe(4826286); + }); + + it("rejects when no policies are returned", () => { + expect(() => selectLatestActiveFanavaranPolicy([], today)).toThrow( + BadRequestException, + ); + }); + + it("rejects when the latest policy is expired", () => { + expect(() => + selectLatestActiveFanavaranPolicy( + [ + { PolicyId: 3719458, EndDate: "1404/09/06" }, + { PolicyId: 2800731, EndDate: "1403/09/05" }, + ], + today, + ), + ).toThrow(BadRequestException); + }); + + it("rejects when the latest policy has no valid PolicyId", () => { + expect(() => + selectLatestActiveFanavaranPolicy( + [ + { PolicyId: 3719458, EndDate: "1404/09/06" }, + { PolicyId: null, EndDate: "1405/09/23" }, + ], + today, + ), + ).toThrow(BadRequestException); + }); +}); diff --git a/src/claim-request-management/fanavaran-policy-selection.ts b/src/claim-request-management/fanavaran-policy-selection.ts new file mode 100644 index 0000000..d9e38cd --- /dev/null +++ b/src/claim-request-management/fanavaran-policy-selection.ts @@ -0,0 +1,95 @@ +import { BadRequestException } from "@nestjs/common"; +import { jalaliToGregorianDate } from "src/helpers/date-jalali"; +import { gregorianDateInIran } from "src/helpers/iran-datetime"; + +export type FanavaranPolicyInquiryRow = { + PolicyId?: unknown; + EndDate?: unknown; +}; + +export type SelectedFanavaranPolicy = { + policy: FanavaranPolicyInquiryRow; + policyId: number; + endDate: string; + endDateGregorian: string; +}; + +type DatedFanavaranPolicy = { + policy: FanavaranPolicyInquiryRow; + endDate: string; + endDateGregorian: string; +}; + +const NO_POLICY_MESSAGE = + "No Fanavaran policies were found for the insurer national code. PolicyId is required; contact the administrator."; + +const EXPIRED_POLICY_MESSAGE = + "The latest insurance policy is expired and cannot be sent to Fanavaran. Contact the administrator."; + +const INVALID_POLICY_MESSAGE = + "Fanavaran policy inquiry returned policies without a valid PolicyId or EndDate. PolicyId is required; contact the administrator."; + +function parsePolicyId(value: unknown): number | null { + if (value === null || value === undefined) return null; + if (typeof value === "string" && value.trim() === "") return null; + const id = Number(value); + return Number.isFinite(id) && id > 0 ? id : null; +} + +function normalizeEndDate(value: unknown): { + endDate: string; + endDateGregorian: string; +} | null { + if (value === null || value === undefined) return null; + const endDate = String(value).trim(); + if (!endDate) return null; + + const endDateGregorian = jalaliToGregorianDate(endDate); + if (!endDateGregorian) return null; + + return { endDate, endDateGregorian }; +} + +export function selectLatestActiveFanavaranPolicy( + policies: unknown, + todayGregorian: string = gregorianDateInIran(new Date()), +): SelectedFanavaranPolicy { + if (!Array.isArray(policies) || policies.length === 0) { + throw new BadRequestException(NO_POLICY_MESSAGE); + } + + const candidates = policies + .map((policy) => { + if (!policy || typeof policy !== "object") return null; + const row = policy as FanavaranPolicyInquiryRow; + const endDate = normalizeEndDate(row.EndDate); + if (!endDate) return null; + return { + policy: row, + endDate: endDate.endDate, + endDateGregorian: endDate.endDateGregorian, + }; + }) + .filter((policy): policy is DatedFanavaranPolicy => policy !== null); + + if (candidates.length === 0) { + throw new BadRequestException(INVALID_POLICY_MESSAGE); + } + + const latest = candidates.reduce((currentLatest, candidate) => + candidate.endDateGregorian > currentLatest.endDateGregorian + ? candidate + : currentLatest, + ); + + if (latest.endDateGregorian < todayGregorian) { + throw new BadRequestException(EXPIRED_POLICY_MESSAGE); + } + + const policyId = parsePolicyId(latest.policy.PolicyId); + if (policyId === null) { + throw new BadRequestException(INVALID_POLICY_MESSAGE); + } + + return { ...latest, policyId }; +}