import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { LogService } from '../log/log.service'; import { ObatService } from '../obat/obat.service'; import { RekammedisService } from '../rekammedis/rekammedis.service'; import { TindakanDokterService } from '../tindakandokter/tindakandokter.service'; import { sha256 } from '@api/common/crypto/hash'; import type { AuditEvent, resultStatus as ResultStatus, } from '@dist/generated/prisma'; type AuditRecordPayload = { id: string; event: AuditEvent; payload: string; timestamp: Date; user_id: bigint; last_sync: Date; result: ResultStatus; }; @Injectable() export class AuditService { constructor( private readonly prisma: PrismaService, private readonly logService: LogService, private readonly obatService: ObatService, private readonly rekamMedisService: RekammedisService, private readonly tindakanDokterService: TindakanDokterService, ) {} async getAuditTrails(pageSize: number, bookmark: string) { const auditLogs = await this.prisma.audit.findMany({ take: pageSize, skip: bookmark ? 1 : 0, cursor: bookmark ? { id: bookmark } : undefined, orderBy: { timestamp: 'asc' }, }); return auditLogs; } async storeAuditTrail() { console.log('Storing audit trail...'); const BATCH_SIZE = 25; let bookmark = ''; let processedCount = 0; try { while (true) { const pageResults = await this.logService.getLogsWithPagination( BATCH_SIZE, bookmark, ); const logs: any[] = Array.isArray(pageResults?.logs) ? pageResults.logs : []; const nextBookmark: string = pageResults?.bookmark ?? ''; if ( logs.length === 0 && (nextBookmark === '' || nextBookmark === bookmark) ) { break; } const records = ( await Promise.all( logs.map((logEntry) => this.buildAuditRecord(logEntry)), ) ).filter((record): record is AuditRecordPayload => record !== null); if (records.length > 0) { await this.prisma.$transaction( records.map((record) => this.prisma.audit.upsert({ where: { id: record.id }, create: record, update: { event: record.event, payload: record.payload, timestamp: record.timestamp, user_id: record.user_id, last_sync: record.last_sync, result: record.result, }, }), ), ); processedCount += records.length; } if (nextBookmark === '' || nextBookmark === bookmark) { break; } bookmark = nextBookmark; } } catch (error) { console.error('Error storing audit trail:', error); throw error; } } private async buildAuditRecord( logEntry: any, ): Promise { if (!logEntry?.value) { return null; } const { value } = logEntry; const logId: string | undefined = value.id; if (!logId) { return null; } const now = new Date(); const timestamp = this.parseTimestamp(value.timestamp) ?? now; const userId = value.user_id; const blockchainHash: string | undefined = value.payload; if (!blockchainHash) { return null; } let dbHash: string | null = null; try { if (logId.startsWith('OBAT_')) { const obatId = this.extractNumericId(logId); if (obatId !== null) { const obat = await this.obatService.getObatById(obatId); if (obat) { dbHash = this.obatService.createHashingPayload({ obat: obat.obat, jumlah_obat: obat.jumlah_obat, aturan_pakai: obat.aturan_pakai, }); } } } else if (logId.startsWith('REKAM')) { const rekamMedisId = this.extractStringId(logId); if (rekamMedisId) { const rekamMedis = await this.rekamMedisService.getRekamMedisById(rekamMedisId); if (rekamMedis) { dbHash = this.rekamMedisService.createHashingPayload({ dokter_id: 123, visit_id: rekamMedis.id_visit ?? '', anamnese: rekamMedis.anamnese ?? '', jenis_kasus: rekamMedis.jenis_kasus ?? '', tindak_lanjut: rekamMedis.tindak_lanjut ?? '', }); } } } else if (logId.startsWith('TINDAKAN')) { const tindakanId = this.extractNumericId(logId); if (tindakanId !== null) { const tindakanDokter = await this.tindakanDokterService.getTindakanDokterById(tindakanId); if (tindakanDokter) { dbHash = this.tindakanDokterService.createHashingPayload({ id_visit: tindakanDokter.id_visit, tindakan: tindakanDokter.tindakan, kategori_tindakan: tindakanDokter.kategori_tindakan, kelompok_tindakan: tindakanDokter.kelompok_tindakan, }); } } } else { return null; } } catch (err) { console.warn(`Failed to resolve related data for log ${logId}:`, err); } const isNotTampered = dbHash ? await this.compareData(blockchainHash, dbHash) : false; const result: ResultStatus = isNotTampered ? 'non_tampered' : 'tampered'; return { id: logId, event: value.event as AuditEvent, payload: blockchainHash, timestamp, user_id: userId, last_sync: now, result, }; } private parseTimestamp(rawTimestamp?: string) { if (!rawTimestamp) { return null; } const parsed = new Date(rawTimestamp); if (Number.isNaN(parsed.getTime())) { return null; } return parsed; } private extractNumericId(rawId: string): number | null { const [, numericPart] = rawId.split('_'); const parsed = parseInt(numericPart, 10); return Number.isNaN(parsed) ? null : parsed; } private extractStringId(rawId: string): string | null { const parts = rawId.split('_'); return parts.length > 1 ? parts[1] : null; } async compareData(blockchainHash: string, postgreHash: string) { return blockchainHash === postgreHash; } }