init
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user