pembayaranModel = new PembayaranModel(); $this->userModel = new UserModel(); $this->db = Database::getInstance(); } public function requestPembayaran(Request $request, Response $response): Response { $data = $request->getParsedBody(); $token = $data['token'] ?? ''; $no_sl = $data['no_sl'] ?? ''; $nama_bank = $data['nama_bank'] ?? ''; $no_rek = $data['no_rek'] ?? ''; $payment_method = $data['payment_method'] ?? 'transfer'; // transfer, qris // Format response awal sama dengan API lama $responseData = [ 'status' => 404, 'pesan' => 'Gagal mendapatkan detail Tagihan anda, silahkan coba beberapa saat lagi' ]; if (empty($token) || empty($no_sl)) { $responseData['pesan'] = 'Token dan nomor SL harus diisi'; return ResponseHelper::custom($response, $responseData, 404); } $pengguna = $this->userModel->findById($token); if (!$pengguna) { $responseData['pesan'] = 'Token tidak Valid. Silahkan Login dan Ulangi transaksi. Terima kasih'; return ResponseHelper::custom($response, $responseData, 404); } // Cek apakah ada pembayaran yang masih aktif $cek_pembayaran = $this->pembayaranModel->findByTokenAndSL($token, $no_sl, 'DIBUAT'); if ($cek_pembayaran && strtotime($cek_pembayaran->waktu_expired) > time()) { $responseData = [ 'status' => 200, 'pesan' => '', 'data' => $cek_pembayaran ]; return ResponseHelper::custom($response, $responseData, 200); } // Jika ada pembayaran yang expired, update status if ($cek_pembayaran) { $this->pembayaranModel->update($cek_pembayaran->id_pembayaran, ['status_bayar' => 'EXPIRED']); } // Buat pembayaran baru $biaya_admin = 0; $promo = 0; $jumlah_unik = 0; // QRIS tidak pakai kode unik // Cek tagihan dari API TIMO $respon = HttpHelper::doCurl('https://timo.tirtaintan.co.id/enquiry/' . $no_sl); if (!$respon || $respon->errno != 0) { $responseData['pesan'] = 'Gagal mendapatkan data tagihan dari server'; return ResponseHelper::custom($response, $responseData, 404); } if (count($respon->data) > 0) { $total_tagihan = 0; foreach ($respon->data as $d) { $total_tagihan += $d->rek_total; $biaya_admin += $pengguna->biaya_admin; } $total_pembayaran = $total_tagihan + $biaya_admin; // Validasi QRIS: hanya untuk transaksi < 70 ribu if ($payment_method === 'qris') { if ($total_pembayaran > 70000) { $responseData['pesan'] = 'QRIS hanya tersedia untuk transaksi di bawah Rp 70.000'; return ResponseHelper::custom($response, $responseData, 404); } // QRIS tidak pakai kode unik $jumlah_unik = 0; } else { // BRI/Manual pakai kode unik $jumlah_unik = KodeHelper::generateKodeUnikPrioritas(); } $ins = [ 'no_trx' => '#TIMO' . $respon->token, 'token' => $token, 'no_sl' => $no_sl, 'nama_bank' => $payment_method === 'qris' ? 'QRIS' : $nama_bank, 'no_rekening' => $no_rek, 'jumlah_tagihan' => (string)$total_tagihan, 'biaya_admin' => (string)$biaya_admin, 'jumlah_unik' => (string)$jumlah_unik, 'promo' => (string)$promo, 'raw_data' => json_encode($respon->data), 'waktu_expired' => date('Y-m-d H:i:s', strtotime('+1 days')), 'status_bayar' => 'DIBUAT', 'tanggal_bayar' => '0000-00-00 00:00:00', 'jumlah_bayar' => '0', 'bukti_transfer' => '', 'tanggal_request' => date('Y-m-d H:i:s'), ]; $pembayaranId = $this->pembayaranModel->create($ins); // Jika QRIS, generate QR code if ($payment_method === 'qris' && $pembayaranId) { $qrisResponse = QrisHelper::createInvoice($ins['no_trx'], (int)$total_pembayaran, false); if ($qrisResponse && isset($qrisResponse['data'])) { $qrisData = $qrisResponse['data']; $qrisRequestDate = date('Y-m-d H:i:s'); $expiredMinutes = QrisHelper::getExpiredMinutes(); $expiredAt = date('Y-m-d H:i:s', strtotime("+{$expiredMinutes} minutes")); // Update pembayaran dengan data QRIS $this->db->update('pembayaran', [ 'qris_qr_code' => $qrisData['qris_content'] ?? '', 'qris_invoiceid' => $qrisData['qris_invoiceid'] ?? '', 'qris_nmid' => $qrisData['qris_nmid'] ?? QrisHelper::getNmid(), 'qris_request_date' => $qrisRequestDate, 'qris_expired_at' => $expiredAt, 'qris_check_count' => 0, 'qris_last_check_at' => null, 'qris_status' => 'unpaid' // Initial status ], 'id_pembayaran = :id', ['id' => $pembayaranId]); $ins['qris_qr_code'] = $qrisData['qris_content'] ?? ''; $ins['qris_invoiceid'] = $qrisData['qris_invoiceid'] ?? ''; $ins['qris_nmid'] = $qrisData['qris_nmid'] ?? QrisHelper::getNmid(); $ins['qris_request_date'] = $qrisRequestDate; $ins['qris_expired_at'] = $expiredAt; $ins['qris_status'] = 'unpaid'; } else { $responseData['pesan'] = 'Gagal generate QRIS, silahkan coba lagi'; return ResponseHelper::custom($response, $responseData, 404); } } if ($total_tagihan > 0) { $responseData = [ 'status' => 200, 'pesan' => '', 'data' => $ins ]; } else { $responseData['pesan'] = "Tidak ada tagihan untuk no SL $no_sl"; } } else { $responseData['pesan'] = "Tidak ada tagihan untuk no SL $no_sl"; } return ResponseHelper::custom($response, $responseData, $responseData['status']); } public function cekPembayaran(Request $request, Response $response): Response { $data = $request->getParsedBody(); $token = $data['token'] ?? ''; $no_sl = $data['no_sl'] ?? ''; // Format response awal sama dengan API lama $responseData = [ 'status' => 404, 'pesan' => 'Gagal mendapatkan detail Tagihan anda, silahkan coba beberapa saat lagi' ]; if (empty($token) || empty($no_sl)) { $responseData['pesan'] = 'Token dan nomor SL harus diisi'; return ResponseHelper::custom($response, $responseData, 404); } $pengguna = $this->userModel->findById($token); if (!$pengguna) { $responseData['pesan'] = 'Token tidak Valid. Silahkan Login dan Ulangi transaksi. Terima kasih'; return ResponseHelper::custom($response, $responseData, 404); } // Cek pembayaran dengan status DIBUAT atau MENUNGGU VERIFIKASI $cek_pembayaran = $this->pembayaranModel->findByTokenAndSL($token, $no_sl, ['DIBUAT', 'MENUNGGU VERIFIKASI']); if ($cek_pembayaran && strtotime($cek_pembayaran->waktu_expired) > time()) { $responseData = [ 'status' => 200, 'pesan' => '', 'data' => $cek_pembayaran ]; } return ResponseHelper::custom($response, $responseData, $responseData['status']); } public function cekTransfer(Request $request, Response $response): Response { $data = $request->getParsedBody(); $token = $data['token'] ?? ''; $no_rek = $data['no_rek'] ?? ''; // Format response awal sama dengan API lama $responseData = [ 'status' => 404, 'pesan' => 'Gagal membatalkan pembayaran, silahkan coba beberapa saat lagi' ]; if (empty($token) || empty($no_rek)) { $responseData['pesan'] = 'Token dan nomor rekening harus diisi'; return ResponseHelper::custom($response, $responseData, 404); } $pengguna = $this->userModel->findById($token); if (!$pengguna) { $responseData['pesan'] = 'Token tidak Valid. Silahkan Login dan Ulangi transaksi. Terima kasih'; return ResponseHelper::custom($response, $responseData, 404); } $cek_pembayaran = $this->pembayaranModel->findByNoTrx($token, $no_rek); if ($cek_pembayaran) { $responseData = [ 'status' => 200, 'pesan' => '', 'data' => $cek_pembayaran ]; } return ResponseHelper::custom($response, $responseData, $responseData['status']); } public function batalPembayaran(Request $request, Response $response): Response { $data = $request->getParsedBody(); $token = $data['token'] ?? ''; $no_rek = $data['no_rek'] ?? ''; // Format response awal sama dengan API lama $responseData = [ 'status' => 404, 'pesan' => 'Gagal membatalkan pembayaran, silahkan coba beberapa saat lagi' ]; if (empty($token) || empty($no_rek)) { $responseData['pesan'] = 'Token dan nomor rekening harus diisi'; return ResponseHelper::custom($response, $responseData, 404); } $pengguna = $this->userModel->findById($token); if (!$pengguna) { $responseData['pesan'] = 'Token tidak Valid. Silahkan Login dan Ulangi transaksi. Terima kasih'; return ResponseHelper::custom($response, $responseData, 404); } $cek_pembayaran = $this->pembayaranModel->findByNoTrx($token, $no_rek); if ($cek_pembayaran) { $this->pembayaranModel->update($cek_pembayaran->id_pembayaran, ['status_bayar' => 'DIBATALKAN']); // Format response sama dengan API lama: status 200, pesan tetap ada (tidak diubah) $responseData['status'] = 200; // Pesan tetap dengan nilai default, tidak diubah (sesuai API lama) } else { $responseData['pesan'] = 'Tidak ada data dengan no SL $'; } return ResponseHelper::custom($response, $responseData, $responseData['status']); } public function confirmPembayaran(Request $request, Response $response): Response { $data = $request->getParsedBody(); $token = $data['token'] ?? ''; $no_rek = $data['no_rek'] ?? ''; // API lama menggunakan no_rek (no_trx) // Format response awal sama dengan API lama $responseData = [ 'status' => 404, 'pesan' => 'Gagal membatalkan pembayaran, silahkan coba beberapa saat lagi' ]; if (empty($token) || empty($no_rek)) { $responseData['pesan'] = 'Token dan nomor rekening harus diisi'; return ResponseHelper::custom($response, $responseData, 404); } $pengguna = $this->userModel->findById($token); if (!$pengguna) { $responseData['pesan'] = 'Token tidak Valid. Silahkan Login dan Ulangi transaksi. Terima kasih'; return ResponseHelper::custom($response, $responseData, 404); } // Cari pembayaran berdasarkan no_trx (no_rek) $cek_pembayaran = $this->pembayaranModel->findByNoTrx($token, $no_rek); if ($cek_pembayaran) { // Update status ke MENUNGGU VERIFIKASI $this->pembayaranModel->update($cek_pembayaran->id_pembayaran, [ 'status_bayar' => 'MENUNGGU VERIFIKASI' ]); // Kirim notifikasi Telegram $pesan = "🔔 *TRANSAKSI BARU*\n\n" . "No. Transaksi: " . $cek_pembayaran->no_trx . "\n" . "No. SL: " . $cek_pembayaran->no_sl . "\n" . "Jumlah: Rp " . number_format($cek_pembayaran->jumlah_tagihan + $cek_pembayaran->biaya_admin, 0, ',', '.') . "\n" . "Status: MENUNGGU VERIFIKASI\n\n" . "Silahkan verifikasi pembayaran."; TelegramHelper::sendToTransactionAdmin($pesan); // Update respon_wa field $this->pembayaranModel->update($cek_pembayaran->id_pembayaran, [ 'respon_wa' => 'TELEGRAM_SENT_' . date('Y-m-d H:i:s') . ' | SUCCESS' ]); // Format response sama dengan API lama: status 200, pesan tetap ada (tidak diubah) $responseData['status'] = 200; // Pesan tetap dengan nilai default, tidak diubah (sesuai API lama) } return ResponseHelper::custom($response, $responseData, $responseData['status']); } public function historyBayar(Request $request, Response $response): Response { $data = $request->getParsedBody(); $token = $data['token'] ?? ''; // Format response awal sama dengan API lama $responseData = [ 'status' => 404, 'pesan' => '-' ]; if (empty($token)) { $responseData['pesan'] = 'Token harus diisi'; return ResponseHelper::custom($response, $responseData, 404); } $pengguna = $this->userModel->findById($token); if (!$pengguna) { $responseData['pesan'] = 'Token tidak Valid'; return ResponseHelper::custom($response, $responseData, 404); } // History bayar hanya menampilkan yang status DIBAYAR (sama dengan API lama) $history = $this->pembayaranModel->getHistoryByToken($token, 'DIBAYAR', 20); $responseData = [ 'status' => 200, 'pesan' => '', 'data' => $history ]; return ResponseHelper::custom($response, $responseData, 200); } /** * POST /timo/cek_status_qris * Check QRIS payment status (user-triggered) */ public function cekStatusQris(Request $request, Response $response): Response { $data = $request->getParsedBody(); $token = $data['token'] ?? ''; $no_sl = $data['no_sl'] ?? ''; $responseData = [ 'status' => 404, 'pesan' => 'Gagal cek status QRIS' ]; if (empty($token) || empty($no_sl)) { $responseData['pesan'] = 'Token dan nomor SL harus diisi'; return ResponseHelper::custom($response, $responseData, 404); } $pengguna = $this->userModel->findById($token); if (!$pengguna) { $responseData['pesan'] = 'Token tidak Valid'; return ResponseHelper::custom($response, $responseData, 404); } // Cari pembayaran QRIS dengan status DIBUAT $pembayaran = $this->db->fetchOne( "SELECT * FROM pembayaran WHERE token = :token AND no_sl = :no_sl AND nama_bank = 'QRIS' AND status_bayar = 'DIBUAT' AND qris_invoiceid IS NOT NULL ORDER BY id_pembayaran DESC LIMIT 1", ['token' => $token, 'no_sl' => $no_sl] ); if (!$pembayaran) { $responseData['pesan'] = 'Pembayaran QRIS tidak ditemukan'; return ResponseHelper::custom($response, $responseData, 404); } // Cek apakah sudah expired if ($pembayaran->qris_expired_at && strtotime($pembayaran->qris_expired_at) < time()) { // Update status ke expired $this->db->update('pembayaran', [ 'qris_status' => 'expired' ], 'id_pembayaran = :id', ['id' => $pembayaran->id_pembayaran]); $responseData['pesan'] = 'QRIS sudah expired'; $responseData['data'] = [ 'status' => 'expired', 'message' => 'QRIS sudah expired. Silahkan buat pembayaran baru.' ]; return ResponseHelper::custom($response, $responseData, 404); } // Cek apakah sudah mencapai max attempts (3) $checkCount = ($pembayaran->qris_check_count ?? 0); if ($checkCount >= 3) { $responseData = [ 'status' => 200, 'pesan' => 'Silahkan upload bukti pembayaran atau hubungi customer service', 'data' => [ 'status' => 'pending_verification', 'check_count' => $checkCount, 'message' => 'Pembayaran belum terdeteksi. Silahkan upload bukti pembayaran atau hubungi CS.', 'show_upload_proof' => true, 'show_contact_cs' => true ] ]; return ResponseHelper::custom($response, $responseData, 200); } // Cek status dari QRIS API dengan retry mechanism $totalBayar = (int)$pembayaran->jumlah_tagihan + (int)$pembayaran->biaya_admin; $transactionDate = $pembayaran->qris_request_date ?? $pembayaran->tanggal_request; $transactionDate = date('Y-m-d', strtotime($transactionDate)); // Format: YYYY-MM-DD // Gunakan checkStatusWithRetry (max 3 attempts, 15 seconds interval) $qrisStatus = QrisHelper::checkStatusWithRetry( (int)$pembayaran->qris_invoiceid, $totalBayar, $transactionDate ); // Update check count $checkCount = $checkCount + 1; $this->db->update('pembayaran', [ 'qris_check_count' => $checkCount, 'qris_last_check_at' => date('Y-m-d H:i:s') ], 'id_pembayaran = :id', ['id' => $pembayaran->id_pembayaran]); // Check response if ($qrisStatus && isset($qrisStatus['status']) && $qrisStatus['status'] == 'success') { $qrisData = $qrisStatus['data'] ?? []; $paymentStatus = $qrisData['qris_status'] ?? 'unpaid'; if ($paymentStatus == 'paid') { // Payment sudah dibayar, auto approve $this->autoApproveQris($pembayaran->id_pembayaran, $qrisData); $responseData = [ 'status' => 200, 'pesan' => 'Pembayaran berhasil', 'data' => [ 'status' => 'paid', 'message' => 'Pembayaran QRIS berhasil diverifikasi', 'payment_method' => $qrisData['qris_payment_methodby'] ?? '', 'customer_name' => $qrisData['qris_payment_customername'] ?? '' ] ]; } else { // Masih unpaid if ($checkCount >= 3) { $responseData = [ 'status' => 200, 'pesan' => 'Silahkan upload bukti pembayaran atau hubungi customer service', 'data' => [ 'status' => 'pending_verification', 'check_count' => $checkCount, 'message' => 'Pembayaran belum terdeteksi setelah 3x pengecekan. Silahkan upload bukti pembayaran.', 'show_upload_proof' => true, 'show_contact_cs' => true ] ]; } else { $responseData = [ 'status' => 200, 'pesan' => 'Menunggu pembayaran', 'data' => [ 'status' => 'unpaid', 'check_count' => $checkCount, 'remaining_attempts' => 3 - $checkCount, 'message' => 'Silahkan scan QR code dan lakukan pembayaran' ] ]; } } } else { // Request gagal atau response tidak valid if ($checkCount >= 3) { $responseData = [ 'status' => 200, 'pesan' => 'Silahkan upload bukti pembayaran atau hubungi customer service', 'data' => [ 'status' => 'pending_verification', 'check_count' => $checkCount, 'message' => 'Gagal mengecek status pembayaran. Silahkan upload bukti pembayaran.', 'show_upload_proof' => true, 'show_contact_cs' => true ] ]; } else { $responseData = [ 'status' => 200, 'pesan' => 'Menunggu pembayaran', 'data' => [ 'status' => 'unpaid', 'check_count' => $checkCount, 'remaining_attempts' => 3 - $checkCount, 'message' => 'Silahkan scan QR code dan lakukan pembayaran' ] ]; } } return ResponseHelper::custom($response, $responseData, 200); } /** * Auto approve QRIS payment setelah verified paid * * @param int $pembayaranId ID pembayaran * @param array $qrisData Data dari QRIS API response */ private function autoApproveQris($pembayaranId, $qrisData = []) { try { $pembayaran = $this->db->fetchOne( "SELECT * FROM pembayaran WHERE id_pembayaran = :id LIMIT 1", ['id' => $pembayaranId] ); if (!$pembayaran || $pembayaran->status_bayar !== 'DIBUAT') { return false; } // Update status ke MENUNGGU VERIFIKASI (sementara) $this->db->update('pembayaran', [ 'status_bayar' => 'MENUNGGU VERIFIKASI', 'qris_status' => 'paid', 'qris_payment_method' => $qrisData['qris_payment_methodby'] ?? '', 'qris_payment_customer_name' => $qrisData['qris_payment_customername'] ?? '' ], 'id_pembayaran = :id', ['id' => $pembayaranId]); // Approve ke PDAM (sama seperti SiteController::approve) $token = str_replace('#TIMO', '', $pembayaran->no_trx); $url = "https://timo.tirtaintan.co.id/payment/$token"; $data = []; $rincian = json_decode($pembayaran->raw_data); if (is_array($rincian) && count($rincian) > 0) { foreach ($rincian as $r) { $data[] = [ 'rek_nomor' => $r->rek_nomor ?? $r->rek_no ?? '', 'rek_total' => $r->rek_total ?? 0, 'serial' => '#TM' . time(), 'byr_tgl' => date('YmdHis'), 'loket' => 'TIMO', ]; } } $post = [ 'token' => $token, 'data' => $data ]; $headers = [ 'Content-Type: application/json', 'Accept-Encoding: gzip, deflate', 'Cache-Control: max-age=0', 'Connection: keep-alive', 'Accept-Language: en-US,en;q=0.8,id;q=0.6' ]; $paymentResponse = HttpHelper::doCurl($url, 'POST', $post, true, $headers); if ($paymentResponse && isset($paymentResponse->errno) && $paymentResponse->errno == 0) { $totalBayar = (int)$pembayaran->jumlah_tagihan + (int)$pembayaran->biaya_admin; $paidAt = date('Y-m-d H:i:s'); $this->db->update('pembayaran', [ 'status_bayar' => 'DIBAYAR', 'tanggal_bayar' => $paidAt, 'jumlah_bayar' => (string)$totalBayar, 'raw_bayar' => json_encode($paymentResponse), 'qris_paid_at' => $paidAt ], 'id_pembayaran = :id', ['id' => $pembayaranId]); // Kirim notifikasi WhatsApp ke user $user = $this->userModel->findById($pembayaran->token); if ($user && $user->no_hp) { $pesan = "✅ *Pembayaran Berhasil*\n\n" . "No. Transaksi: " . $pembayaran->no_trx . "\n" . "No. SL: " . $pembayaran->no_sl . "\n" . "Jumlah: Rp " . number_format($totalBayar, 0, ',', '.') . "\n" . "Metode: QRIS\n" . "E-Wallet: " . ($qrisData['qris_payment_methodby'] ?? '-') . "\n\n" . "Terima kasih telah melakukan pembayaran."; WhatsAppHelper::sendWa($user->no_hp, $pesan); } return true; } return false; } catch (\Exception $e) { error_log("Error in autoApproveQris: " . $e->getMessage()); return false; } } }