fix email sender all flow
This commit is contained in:
@@ -131,6 +131,7 @@ export async function joinTripAction(tripId: string) {
|
||||
try {
|
||||
await requireActiveUser(session.user.id);
|
||||
await tripService.joinTrip(tripId, session.user.id);
|
||||
void notifyJoinRequest(tripId, session.user.id);
|
||||
revalidatePath(`/trips/${tripId}`);
|
||||
revalidatePath("/trips");
|
||||
revalidatePath("/");
|
||||
@@ -212,6 +213,60 @@ async function notifyBookingApproved(participantId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
/** E3.1 — kabari organizer ada peserta baru yang mengajukan join. */
|
||||
async function notifyJoinRequest(tripId: string, joinerId: string) {
|
||||
const [trip, joiner] = await Promise.all([
|
||||
prisma.trip.findUnique({
|
||||
where: { id: tripId },
|
||||
select: {
|
||||
title: true,
|
||||
organizer: { select: { email: true, name: true } },
|
||||
},
|
||||
}),
|
||||
prisma.user.findUnique({
|
||||
where: { id: joinerId },
|
||||
select: { name: true },
|
||||
}),
|
||||
]);
|
||||
if (!trip || !joiner) return;
|
||||
await emailService.send({
|
||||
to: trip.organizer.email,
|
||||
idempotencyKey: `join_request-${tripId}-${joinerId}`,
|
||||
template: {
|
||||
template: "join_request",
|
||||
data: {
|
||||
organizerName: trip.organizer.name,
|
||||
joinerName: joiner.name,
|
||||
tripTitle: trip.title,
|
||||
tripId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** E3.2 — kabari peserta kalau permintaan join-nya ditolak organizer. */
|
||||
async function notifyJoinRejected(participantId: string) {
|
||||
const participant = await prisma.tripParticipant.findUnique({
|
||||
where: { id: participantId },
|
||||
select: {
|
||||
user: { select: { email: true, name: true } },
|
||||
trip: { select: { title: true } },
|
||||
},
|
||||
});
|
||||
if (!participant) return;
|
||||
await emailService.send({
|
||||
to: participant.user.email,
|
||||
idempotencyKey: `join_rejected-${participantId}`,
|
||||
template: {
|
||||
template: "join_rejected",
|
||||
data: {
|
||||
userName: participant.user.name,
|
||||
tripTitle: participant.trip.title,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function rejectParticipantAction(
|
||||
tripId: string,
|
||||
participantId: string
|
||||
@@ -227,6 +282,7 @@ export async function rejectParticipantAction(
|
||||
participantId,
|
||||
session.user.id
|
||||
);
|
||||
void notifyJoinRejected(participantId);
|
||||
revalidatePath(`/trips/${tripId}`);
|
||||
revalidatePath("/trips");
|
||||
revalidatePath("/");
|
||||
@@ -237,6 +293,66 @@ export async function rejectParticipantAction(
|
||||
}
|
||||
}
|
||||
|
||||
type CloseTripResult = Awaited<ReturnType<typeof tripService.closeTrip>>;
|
||||
|
||||
/**
|
||||
* E3.4 / E3.5 (+ E2.4) — kabari semua peserta aktif kalau trip dibatalkan.
|
||||
* Email organizer-cancel & admin-cancel beda template; admin-cancel juga
|
||||
* mengabari organizer. Refund block ikut di email (nominal dari `notify`).
|
||||
*/
|
||||
function notifyTripCancelled(
|
||||
tripId: string,
|
||||
notify: CloseTripResult["notify"],
|
||||
actor: { type: "ORGANIZER" } | { type: "ADMIN"; reason: string }
|
||||
) {
|
||||
for (const p of notify.participants) {
|
||||
if (actor.type === "ORGANIZER") {
|
||||
void emailService.send({
|
||||
to: p.email,
|
||||
idempotencyKey: `trip_cancelled_organizer-${tripId}-${p.userId}`,
|
||||
template: {
|
||||
template: "trip_cancelled_organizer",
|
||||
data: {
|
||||
userName: p.name,
|
||||
tripTitle: notify.tripTitle,
|
||||
refundAmount: p.refundAmount,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
void emailService.send({
|
||||
to: p.email,
|
||||
idempotencyKey: `trip_cancelled_admin-${tripId}-${p.userId}`,
|
||||
template: {
|
||||
template: "trip_cancelled_admin",
|
||||
data: {
|
||||
userName: p.name,
|
||||
tripTitle: notify.tripTitle,
|
||||
reason: actor.reason,
|
||||
refundAmount: p.refundAmount,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
// Admin force-cancel → organizer juga dikabari (E3.5).
|
||||
if (actor.type === "ADMIN") {
|
||||
void emailService.send({
|
||||
to: notify.organizer.email,
|
||||
idempotencyKey: `trip_cancelled_admin-${tripId}-organizer`,
|
||||
template: {
|
||||
template: "trip_cancelled_admin",
|
||||
data: {
|
||||
userName: notify.organizer.name,
|
||||
tripTitle: notify.tripTitle,
|
||||
reason: actor.reason,
|
||||
refundAmount: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelTripAction(tripId: string) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) {
|
||||
@@ -248,6 +364,7 @@ export async function cancelTripAction(tripId: string) {
|
||||
type: "ORGANIZER",
|
||||
userId: session.user.id,
|
||||
});
|
||||
notifyTripCancelled(tripId, result.notify, { type: "ORGANIZER" });
|
||||
revalidatePath(`/trips/${tripId}`);
|
||||
revalidatePath("/trips");
|
||||
revalidatePath("/");
|
||||
@@ -294,6 +411,10 @@ export async function adminCancelTripAction(tripId: string, reason: string) {
|
||||
adminId: session.user.id,
|
||||
reason: trimmedReason,
|
||||
});
|
||||
notifyTripCancelled(tripId, result.notify, {
|
||||
type: "ADMIN",
|
||||
reason: trimmedReason,
|
||||
});
|
||||
await auditLog.record({
|
||||
admin: { id: session.user.id, email: session.user.email },
|
||||
action: "TRIP_ADMIN_CANCEL",
|
||||
|
||||
Reference in New Issue
Block a user