Files
setrip/server/services/trip.service.ts
T
2026-04-20 00:25:05 +07:00

149 lines
4.2 KiB
TypeScript

import { tripRepo } from "@/server/repositories/trip.repo";
import { participantRepo } from "@/server/repositories/participant.repo";
import { LIMITS } from "@/lib/limits";
import { utcStartOfDay, isTripDepartureDayPast } from "@/lib/trip-dates";
interface CreateTripInput {
title: string;
description?: string;
mountain: string;
location: string;
date: Date;
endDate?: Date;
maxParticipants: number;
price: number;
organizerId: string;
imageUrls?: string[];
}
export const tripService = {
async getOpenTrips(filters?: { q?: string; from?: string; to?: string }) {
return tripRepo.findOpen(filters);
},
async getAllTrips() {
return tripRepo.findAll();
},
async getTripById(id: string) {
const trip = await tripRepo.findById(id);
if (!trip) {
throw new Error("Trip tidak ditemukan");
}
return trip;
},
async createTrip(input: CreateTripInput) {
const since = utcStartOfDay(new Date());
const todayCount = await tripRepo.countByOrganizerSince(
input.organizerId,
since
);
if (todayCount >= LIMITS.MAX_TRIPS_PER_ORGANIZER_PER_DAY) {
throw new Error(
`Batas harian: maksimal ${LIMITS.MAX_TRIPS_PER_ORGANIZER_PER_DAY} trip per hari (UTC). Coba lagi besok.`
);
}
if (isTripDepartureDayPast(input.date)) {
throw new Error("Tanggal berangkat tidak boleh di masa lalu");
}
if (input.endDate && input.endDate.getTime() < input.date.getTime()) {
throw new Error("Tanggal pulang tidak boleh sebelum tanggal berangkat");
}
const images = input.imageUrls?.length
? {
create: input.imageUrls.map((url, i) => ({ url, order: i })),
}
: undefined;
return tripRepo.create({
title: input.title,
description: input.description,
mountain: input.mountain,
location: input.location,
date: input.date,
endDate: input.endDate,
maxParticipants: input.maxParticipants,
price: input.price,
organizer: { connect: { id: input.organizerId } },
images,
});
},
async joinTrip(tripId: string, userId: string) {
const trip = await tripRepo.findById(tripId);
if (!trip) {
throw new Error("Trip tidak ditemukan");
}
if (trip.status !== "OPEN") {
throw new Error("Trip tidak tersedia untuk pendaftaran");
}
if (isTripDepartureDayPast(trip.date)) {
throw new Error(
"Trip sudah melewati tanggal berangkat, tidak bisa mendaftar"
);
}
if (trip.organizerId === userId) {
throw new Error("Organizer tidak bisa join trip sendiri");
}
const existing = await participantRepo.findByTripAndUser(tripId, userId);
if (existing && existing.status !== "CANCELLED") {
throw new Error("Kamu sudah terdaftar di trip ini");
}
const participantCount = await participantRepo.countByTrip(tripId);
if (participantCount >= trip.maxParticipants) {
await tripRepo.updateStatus(tripId, "FULL");
throw new Error("Trip sudah penuh");
}
const participant =
existing?.status === "CANCELLED"
? await participantRepo.reactivate(tripId, userId)
: await participantRepo.create(tripId, userId);
const newCount = await participantRepo.countByTrip(tripId);
if (newCount >= trip.maxParticipants) {
await tripRepo.updateStatus(tripId, "FULL");
}
return participant;
},
async cancelJoin(tripId: string, userId: string) {
const trip = await tripRepo.findById(tripId);
if (!trip) {
throw new Error("Trip tidak ditemukan");
}
if (isTripDepartureDayPast(trip.date)) {
throw new Error(
"Tanggal berangkat trip sudah lewat — pendaftaran tidak bisa dibatalkan. Jika trip sudah selesai, gunakan bagian ulasan."
);
}
const existing = await participantRepo.findByTripAndUser(tripId, userId);
if (!existing || existing.status === "CANCELLED") {
throw new Error("Kamu tidak terdaftar di trip ini");
}
const result = await participantRepo.cancel(tripId, userId);
if (trip.status === "FULL") {
const count = await participantRepo.countByTrip(tripId);
if (count < trip.maxParticipants) {
await tripRepo.updateStatus(tripId, "OPEN");
}
}
return result;
},
};