add booking and payment schema

This commit is contained in:
2026-05-08 20:59:01 +07:00
parent c9c4c0e683
commit 2223a4630e
23 changed files with 5618 additions and 184 deletions
+68 -59
View File
@@ -3,6 +3,8 @@ import type { ActivityCategory, Vibe } from "@/app/generated/prisma/enums";
import { prisma } from "@/lib/prisma";
import { tripRepo, type TripFilters } from "@/server/repositories/trip.repo";
import { participantRepo } from "@/server/repositories/participant.repo";
import { bookingRepo } from "@/server/repositories/booking.repo";
import { bookingService } from "@/server/services/booking.service";
import { LIMITS } from "@/lib/limits";
import { utcStartOfDay, isTripDepartureDayPast } from "@/lib/trip-dates";
import { isFreeTrip } from "@/lib/trip-pricing";
@@ -140,6 +142,7 @@ export const tripService = {
date: true,
organizerId: true,
maxParticipants: true,
price: true,
},
});
@@ -186,6 +189,22 @@ export const tripService = {
data: { tripId, userId, status: "PENDING" },
});
// Booking 1-1 ke participant. Upsert untuk handle re-join setelah CANCELLED.
await tx.booking.upsert({
where: { participantId: participant.id },
create: {
tripId,
userId,
participantId: participant.id,
amount: trip.price,
status: "PENDING",
},
update: {
status: "PENDING",
amount: trip.price,
},
});
const newCount = await tx.tripParticipant.count({
where: { tripId, status: { not: "CANCELLED" } },
});
@@ -234,7 +253,21 @@ export const tripService = {
throw new Error("Kamu tidak terdaftar di trip ini");
}
const result = await participantRepo.cancel(tripId, userId);
const result = await prisma.$transaction(async (tx) => {
const cancelled = await tx.tripParticipant.update({
where: { tripId_userId: { tripId, userId } },
data: {
status: "CANCELLED",
markedPaidAt: null,
paymentConfirmedAt: null,
},
});
await tx.booking.updateMany({
where: { participantId: existing.id },
data: { status: "CANCELLED" },
});
return cancelled;
});
if (trip.status === "FULL") {
const count = await participantRepo.countByTrip(tripId);
@@ -267,7 +300,19 @@ export const tripService = {
throw new Error("Peserta ini tidak dalam status menunggu persetujuan");
}
return participantRepo.setStatus(participantId, "CONFIRMED");
// Trip gratis: Booking langsung PAID. Trip berbayar: AWAITING_PAY (tinggal bayar).
const nextBookingStatus = isFreeTrip(trip) ? "PAID" : "AWAITING_PAY";
return prisma.$transaction(async (tx) => {
await tx.booking.updateMany({
where: { participantId },
data: { status: nextBookingStatus },
});
return tx.tripParticipant.update({
where: { id: participantId },
data: { status: "CONFIRMED" },
});
});
},
async rejectParticipant(
@@ -291,10 +336,20 @@ export const tripService = {
throw new Error("Hanya permintaan yang masih menunggu yang bisa ditolak");
}
await participantRepo.setStatusAndClearPayment(
participantId,
"CANCELLED"
);
await prisma.$transaction(async (tx) => {
await tx.tripParticipant.update({
where: { id: participantId },
data: {
status: "CANCELLED",
markedPaidAt: null,
paymentConfirmedAt: null,
},
});
await tx.booking.updateMany({
where: { participantId },
data: { status: "CANCELLED" },
});
});
if (trip.status === "FULL") {
const count = await participantRepo.countByTrip(tripId);
@@ -322,40 +377,12 @@ export const tripService = {
throw new Error("Trip sudah lewat — pembayaran tidak perlu ditandai");
}
const p = await participantRepo.findByTripAndUser(tripId, userId);
if (!p || p.status === "CANCELLED") {
const booking = await bookingRepo.findByTripAndUser(tripId, userId);
if (!booking || booking.status === "CANCELLED") {
throw new Error("Kamu tidak terdaftar di trip ini");
}
if (p.paymentConfirmedAt) {
throw new Error("Pembayaran kamu sudah dikonfirmasi organizer");
}
if (p.markedPaidAt) {
throw new Error("Kamu sudah menandai sudah bayar — tunggu konfirmasi organizer");
}
const updated = await participantRepo.tryMarkPaidByUser(tripId, userId);
if (updated.count === 0) {
const again = await participantRepo.findByTripAndUser(tripId, userId);
if (!again || again.status === "CANCELLED") {
throw new Error("Kamu tidak terdaftar di trip ini");
}
if (again.paymentConfirmedAt) {
throw new Error("Pembayaran kamu sudah dikonfirmasi organizer");
}
if (again.markedPaidAt) {
throw new Error(
"Kamu sudah menandai sudah bayar — tunggu konfirmasi organizer"
);
}
throw new Error("Tidak bisa menandai pembayaran. Coba lagi sebentar.");
}
const row = await participantRepo.findByTripAndUser(tripId, userId);
if (!row) {
throw new Error("Data peserta tidak ditemukan setelah update");
}
return row;
return bookingService.markPaidManual(booking.id, userId);
},
async confirmParticipantPayment(
@@ -374,29 +401,11 @@ export const tripService = {
throw new Error("Trip ini gratis — tidak ada pembayaran yang perlu dikonfirmasi");
}
const participant = await participantRepo.findById(participantId);
if (!participant || participant.tripId !== tripId) {
throw new Error("Peserta tidak ditemukan");
}
if (participant.status === "CANCELLED") {
throw new Error("Peserta sudah tidak aktif");
}
if (!participant.markedPaidAt) {
throw new Error("Peserta belum menandai sudah membayar");
}
if (participant.paymentConfirmedAt) {
throw new Error("Pembayaran peserta ini sudah dikonfirmasi");
const booking = await bookingRepo.findByParticipantId(participantId);
if (!booking || booking.tripId !== tripId) {
throw new Error("Booking tidak ditemukan");
}
const updated = await participantRepo.tryConfirmPaymentByOrganizer(
participantId
);
if (updated.count === 0) {
throw new Error(
"Konfirmasi tidak diproses — mungkin sudah dikonfirmasi atau pembayaran belum ditandai peserta."
);
}
return participantRepo.findById(participantId);
return bookingService.confirmPaidManual(booking.id, organizerId);
},
};