kyc user and upload partial update encrypt nik and picture

This commit is contained in:
2026-04-27 21:48:24 +07:00
parent b31fe675ae
commit a92b4a8fd9
51 changed files with 5180 additions and 452 deletions
+59
View File
@@ -0,0 +1,59 @@
import { prisma } from "@/lib/prisma";
import { Prisma } from "@/app/generated/prisma/client";
export const organizerRepo = {
async findByUserId(userId: string) {
return prisma.organizerVerification.findUnique({ where: { userId } });
},
async findById(id: string) {
return prisma.organizerVerification.findUnique({ where: { id } });
},
async findByNik(nik: string) {
return prisma.organizerVerification.findUnique({ where: { nik } });
},
async upsertForUser(
userId: string,
data: Omit<Prisma.OrganizerVerificationUncheckedCreateInput, "id" | "userId" | "createdAt" | "updatedAt">
) {
return prisma.organizerVerification.upsert({
where: { userId },
create: { userId, ...data },
update: data,
});
},
async listByStatus(status?: "PENDING" | "APPROVED" | "REJECTED") {
return prisma.organizerVerification.findMany({
where: status ? { status } : undefined,
orderBy: { createdAt: "desc" },
include: {
user: { select: { id: true, name: true, email: true } },
reviewedBy: { select: { id: true, name: true, email: true } },
},
});
},
async updateReview(
id: string,
data: {
status: "APPROVED" | "REJECTED";
rejectionReason?: string | null;
reviewedById: string;
}
) {
const now = new Date();
return prisma.organizerVerification.update({
where: { id },
data: {
status: data.status,
rejectionReason: data.rejectionReason ?? null,
reviewedById: data.reviewedById,
reviewedAt: now,
verifiedAt: data.status === "APPROVED" ? now : null,
},
});
},
};
+1 -1
View File
@@ -98,7 +98,7 @@ export const tripRepo = {
name: true,
email: true,
image: true,
isVerified: true,
organizerVerification: { select: { status: true } },
},
},
images: { orderBy: { order: "asc" } },
+8 -1
View File
@@ -2,7 +2,12 @@ import bcrypt from "bcryptjs";
import { userRepo } from "@/server/repositories/user.repo";
export const authService = {
async register(data: { name: string; email: string; password: string }) {
async register(data: {
name: string;
email: string;
password: string;
acceptedTermsAndPrivacy: boolean;
}) {
const existing = await userRepo.findByEmail(data.email);
if (existing) {
throw new Error("Email sudah terdaftar");
@@ -14,6 +19,8 @@ export const authService = {
name: data.name,
email: data.email,
password: hashedPassword,
acceptedTermsAndPrivacy: data.acceptedTermsAndPrivacy,
acceptedAt: data.acceptedTermsAndPrivacy ? new Date() : null,
});
return { id: user.id, name: user.name, email: user.email };
+77
View File
@@ -0,0 +1,77 @@
import { organizerRepo } from "@/server/repositories/organizer.repo";
type SubmitInput = {
fullName: string;
nik: string;
birthDate: Date;
address: string;
ktpImageUrl: string;
selfieUrl: 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 dupNik = await organizerRepo.findByNik(data.nik);
if (dupNik && dupNik.userId !== userId) {
throw new Error("NIK ini sudah dipakai akun lain");
}
return organizerRepo.upsertForUser(userId, {
fullName: data.fullName,
nik: data.nik,
birthDate: data.birthDate,
address: data.address,
ktpImageUrl: data.ktpImageUrl,
selfieUrl: data.selfieUrl,
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";
},
};
+6 -6
View File
@@ -11,11 +11,7 @@ export type OrganizerTrust = {
export const trustService = {
async getOrganizerTrust(organizerId: string): Promise<OrganizerTrust> {
const [user, tripsCreated, reviewAgg] = await Promise.all([
prisma.user.findUnique({
where: { id: organizerId },
select: { isVerified: true },
}),
const [tripsCreated, reviewAgg, organizerVerification] = await Promise.all([
prisma.trip.count({ where: { organizerId } }),
prisma.tripReview.aggregate({
where: {
@@ -24,11 +20,15 @@ export const trustService = {
_avg: { rating: true },
_count: { _all: true },
}),
prisma.organizerVerification.findUnique({
where: { userId: organizerId },
select: { status: true },
}),
]);
const avg = reviewAgg._avg.rating;
return {
isVerified: user?.isVerified ?? false,
isVerified: organizerVerification?.status === "APPROVED",
tripsCreated,
avgRating:
avg != null ? Math.round(Number(avg) * 10) / 10 : null,