This commit is contained in:
Rio
2026-06-18 12:16:27 +07:00
parent 7de3a3b4b1
commit 5c0ab92401
84 changed files with 12562 additions and 285 deletions
@@ -0,0 +1,72 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { getSettings } from '@/lib/settings';
import { sendTicketEmail } from '@/lib/email';
export const revalidate = 0;
export async function GET(
request: Request,
{ params }: { params: { code: string } }
) {
try {
const bookingCode = params.code;
const booking = await prisma.booking.findUnique({
where: { bookingCode },
});
if (!booking) {
return NextResponse.json({ error: 'Pemesanan tidak ditemukan.' }, { status: 404 });
}
if (booking.paymentMethod !== 'QRIS') {
return NextResponse.json({ success: true, status: booking.status });
}
// Call Pakasir API
const settings = await getSettings();
const slug = settings.pakasirSlug;
const apiKey = settings.pakasirApiKey;
const amount = Math.round(Number(booking.totalPrice));
if (!slug || !apiKey) {
// If Pakasir is not fully configured, return current DB status
return NextResponse.json({ success: true, status: booking.status });
}
const pakasirUrl = `https://app.pakasir.com/api/transactiondetail?project=${slug}&amount=${amount}&order_id=${bookingCode}&api_key=${apiKey}`;
const res = await fetch(pakasirUrl, { cache: 'no-store' });
if (!res.ok) {
console.error('Failed to fetch transaction status from Pakasir:', await res.text());
return NextResponse.json({ success: true, status: booking.status }); // Fail silently, return current db status
}
const data = await res.json();
console.log(data);
if (data.transaction && data.transaction.status === 'completed') {
// If status in local DB is not PAID yet, update it and send ticket email
if (booking.status !== 'PAID') {
const updated = await prisma.booking.update({
where: { bookingCode },
data: {
status: 'PAID',
paymentTime: data.transaction.completed_at ? new Date(data.transaction.completed_at) : new Date(),
},
});
// Send email receipt automatically
await sendTicketEmail(booking.id);
return NextResponse.json({ success: true, status: 'PAID', booking: updated });
}
return NextResponse.json({ success: true, status: 'PAID' });
}
return NextResponse.json({ success: true, status: booking.status });
} catch (error) {
console.error('Check Pakasir status error:', error);
return NextResponse.json({ error: 'Gagal mengecek status pembayaran Pakasir.' }, { status: 500 });
}
}
+63
View File
@@ -0,0 +1,63 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { getSettings } from '@/lib/settings';
export async function POST(
request: Request,
{ params }: { params: { code: string } }
) {
try {
const bookingCode = params.code;
const body = await request.json();
const { paymentMethod } = body;
if (!paymentMethod || !['TUNAI', 'QRIS'].includes(paymentMethod)) {
return NextResponse.json({ error: 'Metode pembayaran tidak valid.' }, { status: 400 });
}
const booking = await prisma.booking.findUnique({
where: { bookingCode },
});
if (!booking) {
return NextResponse.json({ error: 'Pemesanan tidak ditemukan.' }, { status: 404 });
}
if (booking.status !== 'PENDING') {
return NextResponse.json({ error: 'Pemesanan ini sudah lunas atau dibatalkan.' }, { status: 400 });
}
if (paymentMethod === 'TUNAI') {
const updated = await prisma.booking.update({
where: { bookingCode },
data: {
paymentMethod: 'TUNAI',
},
});
return NextResponse.json({ success: true, booking: updated });
} else {
// QRIS via Pakasir
const settings = await getSettings();
const slug = settings.pakasirSlug;
const apiKey = settings.pakasirApiKey;
if (!slug || !apiKey) {
return NextResponse.json({ error: 'Pembayaran QRIS belum dikonfigurasi lengkap oleh Admin.' }, { status: 400 });
}
// Record selection in DB
await prisma.booking.update({
where: { bookingCode },
data: {
paymentMethod: 'QRIS',
},
});
return NextResponse.json({ success: true });
}
} catch (error) {
console.error('Process payment error:', error);
return NextResponse.json({ error: 'Gagal memproses metode pembayaran.' }, { status: 500 });
}
}
+65
View File
@@ -0,0 +1,65 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { getSettings } from '@/lib/settings';
export const revalidate = 0;
export async function GET(
request: Request,
{ params }: { params: { code: string } }
) {
try {
const bookingCode = params.code;
const booking = await prisma.booking.findUnique({
where: { bookingCode },
});
if (!booking) {
return NextResponse.json({ error: 'Pemesanan tidak ditemukan.' }, { status: 404 });
}
const settings = await getSettings();
const slug = settings.pakasirSlug;
const apiKey = settings.pakasirApiKey;
if (!slug || !apiKey) {
return NextResponse.json({ error: 'Integrasi Pakasir belum dikonfigurasi lengkap oleh Admin.' }, { status: 400 });
}
const amount = Math.round(Number(booking.totalPrice));
// Call Pakasir direct transaction create API
const res = await fetch('https://app.pakasir.com/api/transactioncreate/qris', {
method: 'POST',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
project: slug,
order_id: bookingCode,
amount: amount,
api_key: apiKey,
}),
});
if (!res.ok) {
const errorText = await res.text();
console.error('Pakasir API Error:', errorText);
return NextResponse.json({ error: 'Gagal membuat QRIS ke penyedia pembayaran.' }, { status: 500 });
}
const data = await res.json();
const paymentNumber = data.payment?.payment_number;
if (!paymentNumber) {
return NextResponse.json({ error: 'Nomor/String QRIS tidak ditemukan dari response penyedia.' }, { status: 500 });
}
return NextResponse.json({ success: true, qrisString: paymentNumber });
} catch (error: any) {
console.error('QRIS Generation Error:', error);
return NextResponse.json({ error: 'Terjadi kesalahan internal server.' }, { status: 500 });
}
}
+46
View File
@@ -0,0 +1,46 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
export async function GET(
request: Request,
{ params }: { params: { code: string } }
) {
try {
const bookingCode = params.code;
const booking = await prisma.booking.findUnique({
where: { bookingCode },
include: {
seats: true,
schedule: {
include: {
route: true,
},
},
},
});
if (!booking) {
return NextResponse.json({ error: 'Booking not found' }, { status: 404 });
}
// Normalize Decimals
const normalizedBooking = {
...booking,
totalPrice: Number(booking.totalPrice),
schedule: {
...booking.schedule,
price: Number(booking.schedule.price),
route: {
...booking.schedule.route,
basePrice: Number(booking.schedule.route.basePrice),
},
},
};
return NextResponse.json({ booking: normalizedBooking });
} catch (error) {
console.error('Fetch booking error:', error);
return NextResponse.json({ error: 'Failed to fetch booking details' }, { status: 500 });
}
}
+96
View File
@@ -0,0 +1,96 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
import { getSessionUser } from '@/lib/auth';
export async function POST(request: Request) {
try {
const body = await request.json();
const {
scheduleId,
passengerName,
passengerEmail,
passengerPhone,
seats, // Array of strings, e.g. ["1", "2"] or ["1A", "1B"]
} = body;
if (!scheduleId || !passengerName || !passengerEmail || !passengerPhone || !seats || !Array.isArray(seats) || seats.length === 0) {
return NextResponse.json(
{ error: 'Missing required booking details or seat selections' },
{ status: 400 }
);
}
// Fetch schedule to get the price
const schedule = await prisma.schedule.findUnique({
where: { id: Number(scheduleId) },
});
if (!schedule) {
return NextResponse.json({ error: 'Schedule not found' }, { status: 404 });
}
// Check optional user session link
const userSession = await getSessionUser();
const userId = userSession ? userSession.userId : null;
// Calculate total price
const unitPrice = Number(schedule.price);
const totalPrice = unitPrice * seats.length;
// Generate unique booking code
const bookingCode = 'TRV-' + Math.random().toString(36).substring(2, 8).toUpperCase();
// Perform database transaction to ensure atomic operations (no double seat bookings)
const booking = await prisma.$transaction(async (tx) => {
// 1. Check if any selected seat is already occupied for this schedule
const occupied = await tx.bookingSeat.findMany({
where: {
scheduleId: Number(scheduleId),
seatNumber: { in: seats },
booking: {
status: { in: ['PENDING', 'PAID'] },
},
},
});
if (occupied.length > 0) {
throw new Error('One or more selected seats have already been booked. Please choose other seats.');
}
// 2. Create the booking record
const newBooking = await tx.booking.create({
data: {
bookingCode,
scheduleId: Number(scheduleId),
userId,
passengerName,
passengerEmail,
passengerPhone,
totalPrice,
status: 'PENDING',
},
});
// 3. Create the booking seat records
const seatRecords = seats.map((seat: string) => ({
bookingId: newBooking.id,
scheduleId: Number(scheduleId),
seatNumber: seat,
}));
await tx.bookingSeat.createMany({
data: seatRecords,
});
return newBooking;
});
return NextResponse.json({ success: true, bookingCode: booking.bookingCode, bookingId: booking.id });
} catch (error: any) {
console.error('Create booking transaction error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to process booking. Please try again.' },
{ status: 400 }
);
}
}