16300, 'SGD' => 12100, 'EUR' => 17600, 'IDR' => 1];
if (!isset($_SESSION['exchange_rates'])) {
$ratesJson = @file_get_contents("https://open.er-api.com/v6/latest/USD");
if ($ratesJson) {
$ratesData = json_decode($ratesJson, true);
if (isset($ratesData['rates'])) {
$_SESSION['exchange_rates'] = $ratesData['rates'];
}
}
}
$rates = $_SESSION['exchange_rates'] ?? $fallbackRates;
$idrRate = $rates['IDR'] ?? 16300;
if ($fromCurrency === 'USD') return $amount * $idrRate;
$fromRate = $rates[$fromCurrency] ?? null;
if ($fromRate) return ($amount / $fromRate) * $idrRate;
return $amount * ($fallbackRates[$fromCurrency] ?? 1);
}
$total_idr_raw = convertToIdr($total_usd, $currency);
$total_idr = round($total_idr_raw);
$fare_idr = round($total_idr * 0.80);
$taxes_idr = $total_idr - $fare_idr;
// FIX: Inisialisasi default agar tidak undefined saat GET (halaman pertama kali load)
$baggage_cost = 0;
$seat_cost = 0;
$service_fee = 15000;
$total_idr_with_extras = $total_idr + $baggage_cost + $seat_cost;
// 5. FORM SUBMISSION HANDLING & PAYMENT REDIRECT
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = isset($_POST['email']) ? trim($_POST['email']) : '';
$phone = isset($_POST['phoneNumber']) ? trim($_POST['phoneNumber']) : '';
$submitted_passengers = isset($_POST['passengers']) && is_array($_POST['passengers']) ? $_POST['passengers'] : [];
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Please provide a valid email address.";
}
if (empty($phone)) {
$errors[] = "Phone number is required.";
}
for ($i = 0; $i < $passenger_count; $i++) {
$pax = $submitted_passengers[$i] ?? [];
$givenName = isset($pax['givenName']) ? trim($pax['givenName']) : '';
$familyName = isset($pax['familyName']) ? trim($pax['familyName']) : '';
$dob = isset($pax['dob']) ? trim($pax['dob']) : '';
if (empty($givenName) || empty($familyName)) {
$errors[] = "Full name of passenger #" . ($i + 1) . " is required.";
}
if (empty($dob)) {
$errors[] = "Date of birth of passenger #" . ($i + 1) . " is required.";
}
$file_field = "passenger_doc_" . $i;
if (isset($_FILES[$file_field]) && $_FILES[$file_field]['error'] === UPLOAD_ERR_OK) {
$fileName = $_FILES[$file_field]['name'];
$fileSize = $_FILES[$file_field]['size'];
$fileNameCmps = explode(".", $fileName);
$fileExtension = strtolower(end($fileNameCmps));
$allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf'];
if (!in_array($fileExtension, $allowedExtensions)) {
$errors[] = "Supported document formats for passenger #" . ($i + 1) . " are limited to JPG, JPEG, PNG, or PDF.";
}
if ($fileSize > 2097152) {
$errors[] = "Document file size for passenger #" . ($i + 1) . " exceeds the maximum limit of 2MB.";
}
} else {
$errors[] = "Identity document (Passport/ID Card) for passenger #" . ($i + 1) . " is required.";
}
}
$uploaded_docs_paths = [];
if (empty($errors)) {
$upload_dir = 'uploads/';
if (!is_dir($upload_dir)) {
@mkdir($upload_dir, 0755, true);
}
for ($i = 0; $i < $passenger_count; $i++) {
$file_field = "passenger_doc_" . $i;
$fileTmpPath = $_FILES[$file_field]['tmp_name'];
$fileName = $_FILES[$file_field]['name'];
$fileNameCmps = explode(".", $fileName);
$fileExtension = strtolower(end($fileNameCmps));
$newFileName = 'pax_' . ($i + 1) . '_' . time() . '_' . bin2hex(random_bytes(8)) . '.' . $fileExtension;
$dest_path = $upload_dir . $newFileName;
if (move_uploaded_file($fileTmpPath, $dest_path)) {
$uploaded_docs_paths[] = [
'passenger_index' => $i + 1,
'original_name' => $fileName,
'file_path' => $dest_path
];
} else {
$errors[] = "Failed to process document upload for passenger #" . ($i + 1);
}
}
}
if (empty($errors)) {
try {
$baggage_cost = 0;
$seat_cost = 0;
$services_summary_lines = [];
$submitted_baggage = isset($_POST['baggage']) && is_array($_POST['baggage']) ? $_POST['baggage'] : [];
for ($i = 0; $i < $passenger_count; $i++) {
$bag_option = $submitted_baggage[$i] ?? 'standard';
$bag_price = 0;
$bag_label = "Standard Included";
switch ($bag_option) {
case '5kg': $bag_price = 150000; $bag_label = "+5 KG Upgrade"; break;
case '10kg': $bag_price = 300000; $bag_label = "+10 KG Upgrade"; break;
case '20kg': $bag_price = 550000; $bag_label = "+20 KG Upgrade"; break;
case '30kg': $bag_price = 800000; $bag_label = "+30 KG Upgrade"; break;
}
if ($bag_price > 0) {
$baggage_cost += $bag_price;
$services_summary_lines[] = "Passenger #" . ($i + 1) . " Baggage: " . $bag_label . " (+Rp " . number_format($bag_price, 0, ',', '.') . ")";
}
}
$submitted_seats = isset($_POST['seats']) && is_array($_POST['seats']) ? $_POST['seats'] : [];
for ($i = 0; $i < $passenger_count; $i++) {
$seat_code = strtoupper(trim($submitted_seats[$i] ?? ''));
if (!empty($seat_code)) {
$seat_price = 0;
$row_num = (int)preg_replace('/[^0-9]/', '', $seat_code);
if ($row_num >= 1 && $row_num <= 2) {
$seat_price = 150000;
$seat_type = "Extra Legroom Seat";
} else {
$seat_price = 50000;
$seat_type = "Standard Seat";
}
$seat_cost += $seat_price;
$services_summary_lines[] = "Passenger #" . ($i + 1) . " Seat: " . $seat_code . " (" . $seat_type . " - +Rp " . number_format($seat_price, 0, ',', '.') . ")";
}
}
$total_idr_with_extras = $total_idr + $baggage_cost + $seat_cost;
$pax_details = [];
foreach ($submitted_passengers as $index => $pax) {
$gender_label = (isset($pax['gender']) && $pax['gender'] === 'f') ? 'Female' : 'Male';
$title_label = ucfirst($pax['title'] ?? 'Mr');
$doc_info = "";
foreach ($uploaded_docs_paths as $doc) {
if ($doc['passenger_index'] === ($index + 1)) {
$doc_info = " (Doc: " . $doc['original_name'] . ")";
break;
}
}
$assigned_seat = !empty($submitted_seats[$index]) ? strtoupper($submitted_seats[$index]) : 'None';
$assigned_bag = !empty($submitted_baggage[$index]) && $submitted_baggage[$index] !== 'standard' ? $submitted_baggage[$index] : 'Standard';
$pax_details[] = "Adult " . ($index + 1) . ": " . $title_label . ". " . strtoupper(($pax['givenName'] ?? '') . ' ' . ($pax['familyName'] ?? '')) . " (" . $gender_label . ", DOB: " . ($pax['dob'] ?? '') . ", Seat: " . $assigned_seat . ", Baggage: " . $assigned_bag . ")" . $doc_info;
}
$special_note = implode("\n", $pax_details);
$meta_key = isset($_POST['meta_key']) ? trim($_POST['meta_key']) : '';
$meta_val = isset($_POST['meta_val']) ? trim($_POST['meta_val']) : '';
if (!empty($meta_key) && !empty($meta_val)) {
$special_note .= "\n\nMetadata: " . $meta_key . " = " . $meta_val;
}
if (!empty($services_summary_lines)) {
$special_note .= "\n\n=== EXTRA SERVICES DETAILS ===\n" . implode("\n", $services_summary_lines);
}
$primary_passenger = strtoupper($submitted_passengers[0]['givenName'] . ' ' . $submitted_passengers[0]['familyName']);
$product_title = $carrier_name . " (" . $origin . " - " . $destination . ")";
// Prepare PDO insertion
$stmtInsert = $pdo->prepare("INSERT INTO bookings (
customer_name,
customer_phone,
customer_email,
guest_qty,
price,
status,
special_note,
type,
product_title,
from_route,
dest_route,
cabin_class,
departure_date,
return_date,
payment_method,
uploaded_docs
) VALUES (
:customer_name,
:customer_phone,
:customer_email,
:guest_qty,
:price,
'pending',
:special_note,
'flight',
:product_title,
:from_route,
:dest_route,
:cabin_class,
:departure_date,
:return_date,
'Bank Transfer',
:uploaded_docs
)");
$stmtInsert->execute([
'customer_name' => $primary_passenger,
'customer_phone' => $phone,
'customer_email' => $email,
'guest_qty' => $passenger_count,
'price' => $total_idr_with_extras,
'special_note' => $special_note,
'product_title' => $product_title,
'from_route' => $origin,
'dest_route' => $destination,
'cabin_class' => $cabinClass,
'departure_date' => $dep_date,
'return_date' => $isReturn ? $retDepDate : null,
'uploaded_docs' => json_encode($uploaded_docs_paths)
]);
$booking_id = $pdo->lastInsertId();
// ========================================================
// AMBIL PENGATURAN MASTER DEFAULT AGAR SINKRON
// ========================================================
$master_logo = 'https://plain-apac-prod-public.komododecks.com/202606/20/zBiz1pT76LPgsDgGiz3k/image.png';
$master_company = 'PT HYTRAVELLERS';
$master_address = "Ruko Keprimall No.46, Sukajadi, Kec. Batam Kota, Kota Batam,\nKepulauan Riau 29444, Indonesia";
$master_pnr = 'Payment Not Verified';
$master_ticket_no = 'Payment Not Verified';
$master_airline_code = strtoupper(substr($carrier_name, 0, 2)) . "-" . rand(100, 999);
try {
$stmtMCheck = $pdo->query("SELECT * FROM invoice_master_settings WHERE id = 1");
$mRow = $stmtMCheck->fetch();
if ($mRow) {
$master_logo = $mRow['invoice_logo_url'] ?: $master_logo;
$master_company = $mRow['company_name'] ?: $master_company;
$master_address = $mRow['company_address'] ?: $master_address;
$master_pnr = $mRow['ticket_pnr'] ?: $master_pnr;
$master_ticket_no = $mRow['ticket_number'] ?: $master_ticket_no;
$master_airline_code = $mRow['airline_code'] ?: $master_airline_code;
}
} catch (Exception $e) {}
// OTOMATIS GENERATE DATA INVOICE BARU BERDASARKAN BOOKING ID
try {
$airports_map = [
'CGK' => ['Jakarta (CGK)', 'Soekarno Hatta Intl. Airport Terminal 3'],
'SIN' => ['Singapore (SIN)', 'Changi International Airport Terminal 4'],
'DPS' => ['Denpasar (DPS)', 'Ngurah Rai Intl. Airport Terminal Domestic'],
'SUB' => ['Surabaya (SUB)', 'Juanda International Airport Terminal 1'],
'KUL' => ['Kuala Lumpur (KUL)', 'Kuala Lumpur International Airport T2'],
];
$f1_from_name = $airports_map[$origin][0] ?? "Airport " . $origin;
$f1_from_detail = $airports_map[$origin][1] ?? "Departures Terminal";
$f1_to_name = $airports_map[$destination][0] ?? "Airport " . $destination;
$f1_to_detail = $airports_map[$destination][1] ?? "Arrivals Terminal";
$f2_carrier = $isReturn ? $retCarrierName : "";
$f2_code = $isReturn ? strtoupper(substr($retCarrierName, 0, 2)) . "-" . rand(100, 999) : "";
$f2_class = $isReturn ? ucfirst($cabinClass) . " (Promo)" : "";
$f2_from_code = $isReturn ? $retOrigin : "";
$f2_from_name = $isReturn ? ($airports_map[$retOrigin][0] ?? "Airport " . $retOrigin) : "";
$f2_from_detail = $isReturn ? ($airports_map[$retOrigin][1] ?? "Departures Terminal") : "";
$f2_to_code = $isReturn ? $retDestination : "";
$f2_to_name = $isReturn ? ($airports_map[$retDestination][0] ?? "Airport " . $retDestination) : "";
$f2_to_detail = $isReturn ? ($airports_map[$retDestination][1] ?? "Arrivals Terminal") : "";
$f2_date = $isReturn ? $retDepDate : "";
$f2_time = $isReturn ? ($retDepTime . " - " . $retArrTime) : "";
$f2_duration = $isReturn ? "Duration " . $retDuration : "";
$f2_baggage = $isReturn ? "Baggage 23 kg" : "";
$inv_price_fee = 15000;
$inv_price_tax = round($total_idr_with_extras * 0.10);
$inv_price_other = 0;
$inv_price_basic = $total_idr_with_extras - $inv_price_fee - $inv_price_tax;
$invoice_num_id = "INV/" . date('Ymd') . "/HT/" . str_pad($booking_id, 6, '0', STR_PAD_LEFT);
$stmtInv = $pdo->prepare("INSERT INTO invoice_settings (
id, invoice_logo_url, company_name, company_address,
footer_no_print_title, footer_no_print_desc, footer_support_phone, footer_support_email, footer_support_web, footer_eco_title, footer_eco_sub,
invoice_id, booking_id, booking_date, issue_date, payment_method, payment_status,
contact_name, contact_email, contact_phone,
passenger_name, passenger_type, passenger_identity,
f1_carrier, f1_code, f1_class, f1_from_code, f1_from_name, f1_from_detail, f1_to_code, f1_to_name, f1_to_detail, f1_date, f1_time, f1_duration, f1_baggage,
f2_carrier, f2_code, f2_class, f2_from_code, f2_from_name, f2_from_detail, f2_to_code, f2_to_name, f2_to_detail, f2_date, f2_time, f2_duration, f2_baggage,
ticket_pnr, ticket_number, airline_code,
price_basic, price_tax, price_other, price_fee, price_total
) VALUES (
:id, :invoice_logo_url, :company_name, :company_address,
:footer_no_print_title, :footer_no_print_desc, :footer_support_phone, :footer_support_email, :footer_support_web, :footer_eco_title, :footer_eco_sub,
:invoice_id, :booking_id, :booking_date, :issue_date, :payment_method, :payment_status,
:contact_name, :contact_email, :contact_phone,
:passenger_name, :passenger_type, :passenger_identity,
:f1_carrier, :f1_code, :f1_class, :f1_from_code, :f1_from_name, :f1_from_detail, :f1_to_code, :f1_to_name, :f1_to_detail, :f1_date, :f1_time, :f1_duration, :f1_baggage,
:f2_carrier, :f2_code, :f2_class, :f2_from_code, :f2_from_name, :f2_from_detail, :f2_to_code, :f2_to_name, :f2_to_detail, :f2_date, :f2_time, :f2_duration, :f2_baggage,
:ticket_pnr, :ticket_number, :airline_code,
:price_basic, :price_tax, :price_other, :price_fee, :price_total
)");
$stmtInv->execute([
'id' => $booking_id,
'invoice_logo_url' => $master_logo,
'company_name' => $master_company,
'company_address' => $master_address,
'footer_no_print_title' => 'Tidak perlu cetak!',
'footer_no_print_desc' => 'E-tiket dapat langsung ditunjukkan melalui HP.',
'footer_support_phone' => '0823 7456 4489 (24 jam)',
'footer_support_email' => 'support@hytravellers.asia',
'footer_support_web' => 'https://hytravellers.asia',
'footer_eco_title' => 'HYTRAVELLERS.ASIA',
'footer_eco_sub' => 'Peduli lingkungan',
'invoice_id' => $invoice_num_id,
'booking_id' => (string)$booking_id,
'booking_date' => date('d M Y, H:i'),
'issue_date' => date('d M Y, H:i'),
'payment_method' => 'Bank Transfer',
'payment_status' => 'PENDING',
'contact_name' => $primary_passenger,
'contact_email' => $email,
'contact_phone' => $phone,
'passenger_name' => $primary_passenger,
'passenger_type' => 'Adult',
'passenger_identity' => 'Identity Document Uploaded',
'f1_carrier' => $carrier_name,
'f1_code' => strtoupper(substr($carrier_name, 0, 2)) . "-" . rand(100, 999),
'f1_class' => ucfirst($cabinClass) . " (Promo)",
'f1_from_code' => $origin,
'f1_from_name' => $f1_from_name,
'f1_from_detail' => $f1_from_detail,
'f1_to_code' => $destination,
'f1_to_name' => $f1_to_name,
'f1_to_detail' => $f1_to_detail,
'f1_date' => $dep_date,
'f1_time' => $dep_time . " - " . $arr_time,
'f1_duration' => "Duration " . $duration,
'f1_baggage' => "Baggage 23 kg",
'f2_carrier' => $f2_carrier,
'f2_code' => $f2_code,
'f2_class' => $f2_class,
'f2_from_code' => $f2_from_code,
'f2_from_name' => $f2_from_name,
'f2_from_detail' => $f2_from_detail,
'f2_to_code' => $f2_to_code,
'f2_to_name' => $f2_to_name,
'f2_to_detail' => $f2_to_detail,
'f2_date' => $f2_date,
'f2_time' => $f2_time,
'f2_duration' => $f2_duration,
'f2_baggage' => $f2_baggage,
'ticket_pnr' => $master_pnr,
'ticket_number' => $master_ticket_no,
'airline_code' => $master_airline_code,
'price_basic' => $inv_price_basic,
'price_tax' => $inv_price_tax,
'price_other' => $inv_price_other,
'price_fee' => $inv_price_fee,
'price_total' => $total_idr_with_extras
]);
} catch (Exception $ex) {}
$_SESSION['order_id'] = $booking_id;
$_SESSION['primary_passenger'] = $primary_passenger;
$_SESSION['stored_total_idr'] = $total_idr_with_extras;
// ========================================================
// INTEGRASI EMAIL: KIRIM EMAIL TAGIHAN SEGERA SETELAH CHECKOUT
// ========================================================
$link_pembayaran = "https://hytravellers.asia/pembayaran.php";
// PRIORITAS 1: Gunakan sendInvoiceAutoEmail dari admin-invoice (smtp_config + email_templates)
$email_sent = false;
try {
$phpmailer_ok = false;
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
$phpmailer_ok = true;
} elseif (file_exists(__DIR__ . '/PHPMailer/PHPMailer.php')) {
require_once __DIR__ . '/PHPMailer/Exception.php';
require_once __DIR__ . '/PHPMailer/PHPMailer.php';
require_once __DIR__ . '/PHPMailer/SMTP.php';
$phpmailer_ok = true;
} elseif (file_exists(__DIR__ . '/PHPMailer/src/PHPMailer.php')) {
require_once __DIR__ . '/PHPMailer/src/Exception.php';
require_once __DIR__ . '/PHPMailer/src/PHPMailer.php';
require_once __DIR__ . '/PHPMailer/src/SMTP.php';
$phpmailer_ok = true;
}
if ($phpmailer_ok) {
$stmt_smtp = $pdo->query("SELECT * FROM smtp_config WHERE id = 1");
$smtp = $stmt_smtp->fetch();
if ($smtp) {
$stmt_tpl = $pdo->prepare("SELECT * FROM email_templates WHERE template_key = 'send_invoice' LIMIT 1");
$stmt_tpl->execute();
$tpl = $stmt_tpl->fetch();
if ($tpl) {
$base_url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
$invoice_url = $base_url . dirname($_SERVER['SCRIPT_NAME']) . '/invoice.php?booking_id=' . $booking_id;
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
$mail->isSMTP();
$mail->Host = $smtp['smtp_host'];
$mail->Port = (int)$smtp['smtp_port'];
$mail->SMTPAuth = true;
$mail->Username = $smtp['smtp_username'];
$mail->Password = $smtp['smtp_password'];
$mail->SMTPSecure = ($smtp['smtp_secure'] === 'ssl') ? \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS : \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10;
$mail->setFrom($smtp['sender_email'], $smtp['sender_name']);
$mail->addReplyTo($smtp['sender_email'], $smtp['sender_name']);
$mail->addAddress($email, $primary_passenger);
$replacements = [
'{nama}' => $primary_passenger,
'{email}' => $email,
'{invoice_id}' => $invoice_num_id,
'{link_invoice}' => $invoice_url,
'{phone}' => '0823 7456 4489 (24 jam)',
'{web}' => 'https://hytravellers.asia',
'{total}' => 'Rp ' . number_format($total_idr_with_extras, 0, ',', '.'),
'{carrier}' => $carrier_name,
'{route}' => $origin . ' → ' . $destination,
'{date}' => $dep_date,
'{time}' => $dep_time,
'{status}' => 'PENDING',
];
$subject = str_replace(array_keys($replacements), array_values($replacements), $tpl['subject']);
$body = str_replace(array_keys($replacements), array_values($replacements), $tpl['body']);
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $body;
$mail->AltBody = strip_tags(str_replace(['
', '
', '
'], "\n", $body));
$mail->send();
$email_sent = true;
}
}
}
} catch (Exception $e_smtp) {
error_log("SMTP email checkout failed: " . $e_smtp->getMessage());
}
// FALLBACK: Kirim via kirim_email.php jika SMTP di atas gagal/tidak tersedia
if (!$email_sent && file_exists('kirim_email.php')) {
try {
require_once 'kirim_email.php';
$replacements = [
'{nama}' => $primary_passenger,
'{booking_id}' => $booking_id,
'{rute_perjalanan}' => $origin . " ➔ " . $destination,
'{tanggal_perjalanan}' => $dep_date,
'{total_pembayaran}' => "Rp " . number_format($total_idr_with_extras, 0, ',', '.'),
'{link_pembayaran}' => $link_pembayaran
];
kirimEmailOtomatis($pdo, $email, 'invoice_bill', $replacements);
} catch (Exception $e_mail) {
error_log("Fallback email checkout failed: " . $e_mail->getMessage());
}
}
header("Location: pembayaran.php");
exit();
} catch (PDOException $e) {
$errors[] = "Failed to register booking in database: " . $e->getMessage();
}
}
}
?>
Checkout – HYTravellers Flight Booking
Search Flights
Booking Checkout
This fare offer will automatically expire on WIB
Selected Route Details
•
-
• Non-stop
Depart from Airport:
Arrive at Airport:
Reschedule Policy
Flight schedule adjustments are permitted up until the departure date without any penalty charges.
•
-
• Non-stop
Depart from Airport:
Arrive at Airport:
Inbound Flight Reschedule Policy
Adjustments on the returning flight route are allowed prior to the original departure date. An administration charge of Rp 300,000 applies.
Order Rescheduling Policy
Rescheduling is permitted before departure with an administrative charge of Rp 300,000 per passenger per route segment.
Global Refund Policy
Refund claims are eligible prior to flight departure subject to deduction fees of Rp 2,250,000 per ticket.
Payment Details
Description
Amount (IDR)
Basic Fare ( Pax)
Rp
Airport Surcharge & Fuel Tax
Rp
Baggage Upgrade Extras
Rp 0
Seat Reservation Extras
Rp 0
Service Charge
Rp 15.000
TOTAL PAYMENT
Rp