import { Injectable, Logger } from "@nestjs/common"; import { InjectModel } from "@nestjs/mongoose"; import { isAxiosError } from "axios"; import { randomBytes } from "node:crypto"; import { Model, Types } from "mongoose"; import { FanavaranAuditLog, FanavaranAuditLogDocument, FanavaranAuditStatus, FanavaranAuditStep, } from "./schema/fanavaran-audit-log.schema"; import type { FanavaranAuditSession } from "./fanavaran-audit.types"; export interface RecordFanavaranAuditStepInput { session: FanavaranAuditSession; step: FanavaranAuditStep; status: FanavaranAuditStatus; requestUrl?: string; httpStatus?: number; requestMeta?: Record; responseMeta?: Record; errorMessage?: string; errorDetails?: Record; durationMs?: number; } @Injectable() export class FanavaranAuditService { private readonly logger = new Logger(FanavaranAuditService.name); constructor( @InjectModel(FanavaranAuditLog.name) private readonly auditModel: Model, ) {} generateTrackingCode(): string { const date = new Date().toISOString().slice(0, 10).replace(/-/g, ""); const suffix = randomBytes(3).toString("hex").toUpperCase(); return `FNV-${date}-${suffix}`; } maskNationalCode(nationalCode: string): string { const trimmed = nationalCode.trim(); if (trimmed.length <= 4) { return "****"; } return `${trimmed.slice(0, 3)}****${trimmed.slice(-2)}`; } sanitizeErrorDetails(error: unknown): Record { if (isAxiosError(error)) { const data = error.response?.data; return { type: "axios", status: error.response?.status, statusText: error.response?.statusText, data: typeof data === "object" && data !== null ? data : typeof data === "string" ? data.slice(0, 2000) : data, }; } if (error instanceof Error) { return { type: "error", name: error.name, message: error.message }; } return { type: "unknown", value: String(error) }; } extractErrorMessage(error: unknown): string { if (isAxiosError(error)) { const data = error.response?.data as | { Message?: string; message?: string } | string | undefined; if (typeof data === "string") { return data; } return ( data?.Message || data?.message || error.message || "Fanavaran request failed" ); } if (error instanceof Error) { return error.message; } return "Fanavaran request failed"; } formatErrorWithTrackingCode(message: string, trackingCode?: string): string { if (!trackingCode) { return message; } return `${message} (trackingCode: ${trackingCode})`; } async recordStep(input: RecordFanavaranAuditStepInput): Promise { try { await this.auditModel.create({ trackingCode: input.session.trackingCode, step: input.step, status: input.status, clientKey: input.session.clientKey, source: input.session.source, ...(input.session.claimCaseId ? { claimCaseId: new Types.ObjectId(input.session.claimCaseId) } : {}), ...(input.session.claimRequestId ? { claimRequestId: new Types.ObjectId(input.session.claimRequestId) } : {}), requestUrl: input.requestUrl, httpStatus: input.httpStatus, requestMeta: input.requestMeta, responseMeta: input.responseMeta, errorMessage: input.errorMessage, errorDetails: input.errorDetails, durationMs: input.durationMs, }); } catch (error) { this.logger.error( `Failed to persist Fanavaran audit step ${input.step} (${input.status})`, error, ); } } async findByTrackingCode(trackingCode: string) { return this.auditModel .find({ trackingCode }) .sort({ createdAt: 1 }) .lean() .exec(); } }