fix email sender all flow

This commit is contained in:
2026-05-20 15:25:32 +07:00
parent 306396ae43
commit cb03967deb
20 changed files with 1450 additions and 62 deletions
+121
View File
@@ -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",