import { organizerRepo } from "@/server/repositories/organizer.repo"; import { decryptString, encryptString, hmacHex } from "@/lib/crypto"; type SubmitInput = { fullName: string; nik: string; birthDate: Date; address: string; ktpImageKey: string; livenessKey: string; bankName: string; bankAccountNumber: string; bankAccountName: string; }; export const organizerService = { async submitVerification(userId: string, data: SubmitInput) { const existing = await organizerRepo.findByUserId(userId); if (existing && existing.status === "APPROVED") { throw new Error("Akun kamu sudah terverifikasi"); } if (existing && existing.status === "PENDING") { throw new Error("Pengajuan kamu masih dalam proses review"); } const nikHash = hmacHex(data.nik); const dupNik = await organizerRepo.findByNikHash(nikHash); if (dupNik && dupNik.userId !== userId) { throw new Error("NIK ini sudah dipakai akun lain"); } return organizerRepo.upsertForUser(userId, { fullName: data.fullName, nikEncrypted: encryptString(data.nik), nikHash, birthDate: data.birthDate, address: data.address, ktpImageKey: data.ktpImageKey, livenessKey: data.livenessKey, bankName: data.bankName, bankAccountNumber: data.bankAccountNumber, bankAccountName: data.bankAccountName, status: "PENDING", rejectionReason: null, reviewedAt: null, reviewedById: null, verifiedAt: null, }); }, async reviewVerification(input: { verificationId: string; decision: "APPROVED" | "REJECTED"; rejectionReason?: string; reviewerId: string; }) { const verification = await organizerRepo.findById(input.verificationId); if (!verification) { throw new Error("Pengajuan tidak ditemukan"); } if (verification.status !== "PENDING") { throw new Error("Pengajuan ini sudah diproses"); } return organizerRepo.updateReview(input.verificationId, { status: input.decision, rejectionReason: input.decision === "REJECTED" ? input.rejectionReason ?? null : null, reviewedById: input.reviewerId, }); }, async getStatusForUser(userId: string) { return organizerRepo.findByUserId(userId); }, async isApproved(userId: string) { const v = await organizerRepo.findByUserId(userId); return v?.status === "APPROVED"; }, /** * Buka kembali verifikasi yang REJECTED ke PENDING. Dipakai admin saat * organizer kirim foto/data baru via email/WA dan ingin di-review ulang * tanpa drop & recreate row. * * - `rejectionReason` lama disimpan di field — kalau di-reject lagi nanti, * field-nya di-overwrite (audit trail via auditLog Phase 4 di roadmap). * - Reset `reviewedById`, `reviewedAt` supaya muncul lagi di tab PENDING. */ async reopenVerification(input: { verificationId: string; adminId: string; note: string; }) { const verification = await organizerRepo.findById(input.verificationId); if (!verification) { throw new Error("Pengajuan tidak ditemukan"); } if (verification.status !== "REJECTED") { throw new Error("Hanya pengajuan REJECTED yang bisa dibuka kembali"); } const trimmedNote = input.note.trim(); if (trimmedNote.length < 10) { throw new Error("Catatan reopen wajib min 10 karakter untuk audit"); } if (trimmedNote.length > 500) { throw new Error("Catatan reopen maksimal 500 karakter"); } return organizerRepo.reopen(input.verificationId, trimmedNote); }, /** Reveal NIK plaintext. Caller must enforce authorization (owner or admin). */ decryptNik(nikEncrypted: string): string { return decryptString(nikEncrypted); }, };