import { prisma } from "@/lib/prisma"; import { COMPLETION_RATE_MIN_SAMPLE, TRIP_LEADER_MIN_TRIPS, } from "@/lib/trust"; export type RatingBreakdown = Record<1 | 2 | 3 | 4 | 5, number>; export type OrganizerTrust = { isVerified: boolean; tripsCreated: number; /// Trip yang sudah lewat tanggal selesai-nya AND tidak dibatalkan (status != CLOSED). /// Dihitung on-the-fly dari endDate (fallback ke date kalau endDate null) — tidak /// bergantung pada Trip.status = COMPLETED yang saat ini belum pernah di-set. tripsCompleted: number; /// Trip dengan status = CLOSED (dibatalkan organizer). tripsCancelled: number; /// Akumulasi peserta CONFIRMED di seluruh trip yang sudah selesai. totalParticipantsServed: number; /// tripsCompleted / (tripsCompleted + tripsCancelled). Null kalau sample /// < COMPLETION_RATE_MIN_SAMPLE — mencegah angka menyesatkan untuk /// organizer yang masih sedikit history-nya. completionRate: number | null; avgRating: number | null; reviewCount: number; ratingBreakdown: RatingBreakdown; isTripLeader: boolean; }; export const trustService = { async getOrganizerTrust(organizerId: string): Promise { const now = new Date(); // Filter "trip yang sudah lewat": endDate < now, atau (endDate null AND date < now). // Trip multi-hari pakai endDate; trip 1 hari biasanya endDate null jadi fallback ke date. const pastTripFilter = { OR: [ { endDate: { lt: now } }, { AND: [{ endDate: null }, { date: { lt: now } }] }, ], }; const [ tripsCreated, tripsCompleted, tripsCancelled, totalParticipantsServed, reviewAgg, ratingGroups, organizerVerification, ] = await Promise.all([ prisma.trip.count({ where: { organizerId } }), prisma.trip.count({ where: { organizerId, status: { not: "CLOSED" }, ...pastTripFilter, }, }), prisma.trip.count({ where: { organizerId, status: "CLOSED" }, }), prisma.tripParticipant.count({ where: { status: "CONFIRMED", trip: { organizerId, status: { not: "CLOSED" }, ...pastTripFilter, }, }, }), prisma.tripReview.aggregate({ where: { trip: { organizerId } }, _avg: { rating: true }, _count: { _all: true }, }), prisma.tripReview.groupBy({ by: ["rating"], where: { trip: { organizerId } }, _count: { _all: true }, }), prisma.organizerVerification.findUnique({ where: { userId: organizerId }, select: { status: true }, }), ]); const ratingBreakdown: RatingBreakdown = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, }; for (const row of ratingGroups) { const r = row.rating; if (r >= 1 && r <= 5) { ratingBreakdown[r as 1 | 2 | 3 | 4 | 5] = row._count._all; } } const completionSample = tripsCompleted + tripsCancelled; const completionRate = completionSample >= COMPLETION_RATE_MIN_SAMPLE ? tripsCompleted / completionSample : null; const avg = reviewAgg._avg.rating; return { isVerified: organizerVerification?.status === "APPROVED", tripsCreated, tripsCompleted, tripsCancelled, totalParticipantsServed, completionRate, avgRating: avg != null ? Math.round(Number(avg) * 10) / 10 : null, reviewCount: reviewAgg._count._all, ratingBreakdown, isTripLeader: tripsCreated >= TRIP_LEADER_MIN_TRIPS, }; }, };