audit fanavaran logs and missing fields null fixed

This commit is contained in:
2026-06-23 15:58:49 +03:30
parent bc70a8c930
commit 8bf3cfe5e9
7 changed files with 443 additions and 24 deletions

View File

@@ -0,0 +1,18 @@
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { FanavaranAuditService } from "./fanavaran-audit.service";
import {
FanavaranAuditLog,
FanavaranAuditLogSchema,
} from "./schema/fanavaran-audit-log.schema";
@Module({
imports: [
MongooseModule.forFeature([
{ name: FanavaranAuditLog.name, schema: FanavaranAuditLogSchema },
]),
],
providers: [FanavaranAuditService],
exports: [FanavaranAuditService],
})
export class FanavaranAuditModule {}

View File

@@ -0,0 +1,137 @@
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<string, unknown>;
responseMeta?: Record<string, unknown>;
errorMessage?: string;
errorDetails?: Record<string, unknown>;
durationMs?: number;
}
@Injectable()
export class FanavaranAuditService {
private readonly logger = new Logger(FanavaranAuditService.name);
constructor(
@InjectModel(FanavaranAuditLog.name)
private readonly auditModel: Model<FanavaranAuditLogDocument>,
) {}
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<string, unknown> {
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<void> {
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();
}
}

View File

@@ -0,0 +1,10 @@
import type { FanavaranClientKey } from "src/core/config/fanavaran-client.config";
import { FanavaranAuditSource } from "./schema/fanavaran-audit-log.schema";
export interface FanavaranAuditSession {
trackingCode: string;
clientKey: FanavaranClientKey;
source: FanavaranAuditSource;
claimCaseId?: string;
claimRequestId?: string;
}

View File

@@ -1,9 +1,10 @@
import { Module } from "@nestjs/common";
import { ClaimRequestManagementModule } from "src/claim-request-management/claim-request-management.module";
import { FanavaranAuditModule } from "./fanavaran-audit.module";
import { FanavaranController } from "./fanavaran.controller";
@Module({
imports: [ClaimRequestManagementModule],
imports: [ClaimRequestManagementModule, FanavaranAuditModule],
controllers: [FanavaranController],
})
export class FanavaranModule {}

View File

@@ -0,0 +1,75 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument, Types } from "mongoose";
import type { FanavaranClientKey } from "src/core/config/fanavaran-client.config";
export enum FanavaranAuditStep {
GET_APP_TOKEN = "GET_APP_TOKEN",
LOGIN = "LOGIN",
POLICY_INQUIRY = "POLICY_INQUIRY",
BUILD_PAYLOAD = "BUILD_PAYLOAD",
SUBMIT_CLAIM = "SUBMIT_CLAIM",
}
export enum FanavaranAuditStatus {
STARTED = "started",
SUCCESS = "success",
FAILURE = "failure",
}
export enum FanavaranAuditSource {
PREVIEW = "preview",
SUBMIT = "submit",
AUTO_SUBMIT = "auto_submit",
LEGACY_SUBMIT = "legacy_submit",
}
@Schema({ collection: "fanavaranAuditLogs", timestamps: true })
export class FanavaranAuditLog {
@Prop({ type: String, required: true, index: true })
trackingCode: string;
@Prop({ type: String, required: true, enum: FanavaranAuditStep, index: true })
step: FanavaranAuditStep;
@Prop({ type: String, required: true, enum: FanavaranAuditStatus, index: true })
status: FanavaranAuditStatus;
@Prop({ type: String, required: true, index: true })
clientKey: FanavaranClientKey;
@Prop({ type: String, required: true, enum: FanavaranAuditSource, index: true })
source: FanavaranAuditSource;
@Prop({ type: Types.ObjectId, required: false, index: true })
claimCaseId?: Types.ObjectId;
@Prop({ type: Types.ObjectId, required: false, index: true })
claimRequestId?: Types.ObjectId;
@Prop({ type: String, required: false })
requestUrl?: string;
@Prop({ type: Number, required: false })
httpStatus?: number;
@Prop({ type: Object, required: false })
requestMeta?: Record<string, unknown>;
@Prop({ type: Object, required: false })
responseMeta?: Record<string, unknown>;
@Prop({ type: String, required: false })
errorMessage?: string;
@Prop({ type: Object, required: false })
errorDetails?: Record<string, unknown>;
@Prop({ type: Number, required: false })
durationMs?: number;
}
export type FanavaranAuditLogDocument = HydratedDocument<FanavaranAuditLog>;
export const FanavaranAuditLogSchema =
SchemaFactory.createForClass(FanavaranAuditLog);
FanavaranAuditLogSchema.index({ trackingCode: 1, createdAt: 1 });