126 lines
3.6 KiB
TypeScript
126 lines
3.6 KiB
TypeScript
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<OrganizerTrust> {
|
|
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,
|
|
};
|
|
},
|
|
};
|