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
+33
View File
@@ -183,6 +183,10 @@ async function applyGatewayStatus(
if (newStatus === "PAID" && !isConflict) {
void notifyPaymentPaid(payment.id);
}
// E3.3 — pembayaran kadaluarsa/gagal: kabari user supaya bisa retry.
if (newStatus === "EXPIRED" || newStatus === "FAILED") {
void notifyPaymentFailed(payment.id);
}
return {
ok: true,
@@ -504,6 +508,35 @@ async function notifyPaymentPaid(paymentId: string) {
});
}
/** E3.3 — kabari user kalau pembayaran expired/gagal supaya bisa retry. */
async function notifyPaymentFailed(paymentId: string) {
const payment = await prisma.payment.findUnique({
where: { id: paymentId },
include: {
booking: {
include: {
user: { select: { email: true, name: true } },
trip: { select: { id: true, title: true } },
},
},
},
});
if (!payment) return;
await emailService.send({
to: payment.booking.user.email,
idempotencyKey: `payment_expired-${payment.id}`,
template: {
template: "payment_expired",
data: {
userName: payment.booking.user.name,
tripTitle: payment.booking.trip.title,
tripId: payment.booking.trip.id,
amount: payment.amount,
},
},
});
}
// Re-export untuk testing langsung kalau perlu (tetap private dari modul lain).
export const _internal = { applyGatewayStatus };
export type { MidtransTransactionStatus };
+28
View File
@@ -1,6 +1,7 @@
import { Prisma } from "@/app/generated/prisma/client";
import { prisma } from "@/lib/prisma";
import { payoutRepo } from "@/server/repositories/payout.repo";
import { emailService } from "@/lib/email/send";
const SERIAL_TX_ATTEMPTS = 6;
@@ -134,6 +135,33 @@ export const payoutService = {
where: { id: { in: ids }, status: "HELD" },
data: { status: "RELEASED", releasedAt: now },
});
// E3.6 — kabari organizer payout-nya sudah lepas hold & masuk antrian
// transfer. Pin ke `releasedAt: now` supaya hanya yang baru di-release.
const released = await prisma.payout.findMany({
where: { id: { in: ids }, status: "RELEASED", releasedAt: now },
select: {
id: true,
amount: true,
organizer: { select: { email: true, name: true } },
trip: { select: { title: true } },
},
});
for (const p of released) {
void emailService.send({
to: p.organizer.email,
idempotencyKey: `payout_released-${p.id}`,
template: {
template: "payout_released",
data: {
organizerName: p.organizer.name,
tripTitle: p.trip.title,
amount: p.amount,
},
},
});
}
return { releasedIds: ids };
},
+28 -1
View File
@@ -458,7 +458,14 @@ export const tripService = {
return runSerializable(async (tx) => {
const trip = await tx.trip.findUnique({
where: { id: tripId },
select: { id: true, status: true, organizerId: true, date: true },
select: {
id: true,
status: true,
organizerId: true,
date: true,
title: true,
organizer: { select: { id: true, email: true, name: true } },
},
});
if (!trip) {
throw new Error("Trip tidak ditemukan");
@@ -500,6 +507,8 @@ export const tripService = {
const refundsCreated: string[] = [];
const cancelledBookings: string[] = [];
const skippedBookings: string[] = [];
// userId → nominal refund yang dibuat untuk dia (untuk email pembatalan).
const refundByUser = new Map<string, number>();
for (const b of bookings) {
if (b.status === "CANCELLED" || b.status === "EXPIRED") {
@@ -540,6 +549,7 @@ export const tripService = {
}
);
refundsCreated.push(refund.id);
refundByUser.set(b.userId, remaining);
// Trip dibatalkan → cancel payout HELD/RELEASED organizer untuk
// booking ini. Payout PAID di-flag clawback otomatis.
@@ -560,6 +570,12 @@ export const tripService = {
}
}
// Snapshot peserta aktif SEBELUM di-cancel — untuk notifikasi email.
const activeParticipants = await tx.tripParticipant.findMany({
where: { tripId, status: { not: "CANCELLED" } },
select: { user: { select: { id: true, email: true, name: true } } },
});
// Semua participant aktif → CANCELLED (apapun status booking-nya).
await tx.tripParticipant.updateMany({
where: { tripId, status: { not: "CANCELLED" } },
@@ -586,6 +602,17 @@ export const tripService = {
refundsCreated,
cancelledBookings,
skippedBookings,
// Data penerima notifikasi — email dikirim oleh action setelah tx commit.
notify: {
tripTitle: trip.title,
organizer: trip.organizer,
participants: activeParticipants.map((p) => ({
userId: p.user.id,
email: p.user.email,
name: p.user.name,
refundAmount: refundByUser.get(p.user.id) ?? 0,
})),
},
};
}, "Gagal membatalkan trip. Coba lagi sebentar.");
},