trust roadmap
This commit is contained in:
@@ -3,6 +3,10 @@ import { participantRepo } from "@/server/repositories/participant.repo";
|
||||
import { reviewRepo } from "@/server/repositories/review.repo";
|
||||
import { isPastTripLastDayForReview } from "@/lib/trip-dates";
|
||||
|
||||
export type OrganizerReviewItem = Awaited<
|
||||
ReturnType<typeof reviewRepo.findByOrganizer>
|
||||
>[number];
|
||||
|
||||
export const reviewService = {
|
||||
async upsertReview(
|
||||
tripId: string,
|
||||
@@ -41,4 +45,11 @@ export const reviewService = {
|
||||
comment: input.comment ?? null,
|
||||
});
|
||||
},
|
||||
|
||||
async getReviewsByOrganizer(
|
||||
organizerId: string,
|
||||
limit?: number
|
||||
): Promise<OrganizerReviewItem[]> {
|
||||
return reviewRepo.findByOrganizer(organizerId, limit);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,38 +1,124 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { TRIP_LEADER_MIN_TRIPS } from "@/lib/trust";
|
||||
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 [tripsCreated, reviewAgg, organizerVerification] = await Promise.all([
|
||||
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.tripReview.aggregate({
|
||||
prisma.trip.count({
|
||||
where: {
|
||||
trip: { organizerId },
|
||||
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,
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user