Spaces:
Sleeping
Sleeping
| const express = require('express'); | |
| const path = require('path'); | |
| const app = express(); | |
| app.use(express.json()); | |
| // Tempat menyimpan status transaksi sementara di memory server | |
| let databaseTransaksi = {}; | |
| const QRIS_BISNIS_BASE = "00020101021226570011ID.DANA.WWW011893600915092050870402099205087040303UMI51440014ID.CO.QRIS.WWW0215ID10254080885610303UMI520448145303360540115802ID5910FAREL SHOP6012Kota Cirebon610545122621960150011ID.DANA.WWW6304"; | |
| function hitungCRC16(str) { | |
| let crc = 0xFFFF; | |
| for (let c = 0; c < str.length; c++) { | |
| let code = str.charCodeAt(c); | |
| crc ^= (code << 8); | |
| for (let i = 0; i < 8; i++) { | |
| if ((crc & 0x8000) !== 0) { | |
| crc = ((crc << 1) ^ 0x1021) & 0xFFFF; | |
| } else { | |
| crc = (crc << 1) & 0xFFFF; | |
| } | |
| } | |
| } | |
| let hex = crc.toString(16).toUpperCase(); | |
| return hex.padStart(4, '0'); | |
| } | |
| // 1. ENDPOINT GENERATE QRIS DYNAMIC | |
| app.get('/create', (req, res) => { | |
| const nominal = req.query.nominal; | |
| if (!nominal || isNaN(nominal) || parseInt(nominal) <= 0) { | |
| return res.status(400).json({ error: 'Nominal harus berupa angka!' }); | |
| } | |
| // Pastikan kunci disimpan sebagai String Angka Murni yang Bersih (Tanpa Spasi/Titik) | |
| const valueUang = parseInt(nominal, 10).toString(); | |
| const lengthUang = valueUang.length.toString().padStart(2, '0'); | |
| const tagNominalBaru = `54${lengthUang}${valueUang}`; | |
| let qrisTanpaCRC = ""; | |
| if (QRIS_BISNIS_BASE.includes('54011')) { | |
| qrisTanpaCRC = QRIS_BISNIS_BASE.replace('54011', tagNominalBaru); | |
| } else { | |
| const posisiID58 = QRIS_BISNIS_BASE.indexOf('5802ID'); | |
| const bagianAwal = QRIS_BISNIS_BASE.substring(0, posisiID58); | |
| const bagianAkhir = QRIS_BISNIS_BASE.substring(posisiID58); | |
| qrisTanpaCRC = bagianAwal + tagNominalBaru + bagianAkhir; | |
| } | |
| const hasilCRC = hitungCRC16(qrisTanpaCRC); | |
| const qrisDynamicString = qrisTanpaCRC + hasilCRC; | |
| // Daftarkan nominal ke database dengan key string angka bersih | |
| databaseTransaksi[valueUang] = { | |
| status: "PENDING", | |
| waktu: new Date() | |
| }; | |
| return res.json({ | |
| status: 'success', | |
| nominal: parseInt(valueUang, 10), | |
| qris_string: qrisDynamicString | |
| }); | |
| }); | |
| // 2. ENDPOINT CHECK STATUS (Real-time & Manual lewat satu pintu) | |
| app.get('/check-status', (req, res) => { | |
| if (!req.query.nominal) return res.status(400).json({ status: "ERROR" }); | |
| const nominal = parseInt(req.query.nominal, 10).toString(); // Normalisasi input key | |
| if (databaseTransaksi[nominal]) { | |
| return res.json({ status: databaseTransaksi[nominal].status }); | |
| } | |
| return res.json({ status: "NOT_FOUND" }); | |
| }); | |
| // 3. ENDPOINT BATALKAN TRANSAKSI | |
| app.get('/cancel', (req, res) => { | |
| if (!req.query.nominal) return res.status(400).json({ status: "ERROR" }); | |
| const nominal = parseInt(req.query.nominal, 10).toString(); | |
| if (databaseTransaksi[nominal]) { | |
| delete databaseTransaksi[nominal]; // Hapus antrean di server | |
| return res.json({ status: "CANCELLED" }); | |
| } | |
| return res.json({ status: "NOT_FOUND" }); | |
| }); | |
| // Ganti bagian app.post('/notifdana') di index.js Hugging Face Anda dengan versi Super-Fix ini: | |
| app.post('/notifdana', (req, res) => { | |
| const { message } = req.body; | |
| console.log(`[LOG MASUK] Pesan dari HP: "${message}"`); | |
| if (message) { | |
| try { | |
| // STRATEGI BARU: | |
| // Ambil bagian teks yang ada di depan sebelum kata "dari" (karena nominal Rp pasti di depan) | |
| // Contoh: "Rp1 dari Gopay..." diambil "Rp1" | |
| // Contoh: "Rp50.000 dari..." diambil "Rp50.000" | |
| const bagianDepan = message.split(/dari/i)[0]; | |
| // Ambil semua karakter angka saja dari bagian depan tersebut | |
| const angkaSaja = bagianDepan.replace(/\D/g, ''); // Menghapus semua yang BUKAN angka (Rp, titik, spasi hilang) | |
| if (angkaSaja) { | |
| const keyTransaksi = parseInt(angkaSaja, 10).toString(); | |
| console.log(`[HASIL EKSTRAKSI] Mencari nominal antrean: "${keyTransaksi}"`); | |
| if (databaseTransaksi[keyTransaksi]) { | |
| databaseTransaksi[keyTransaksi].status = "SUCCESS"; | |
| console.log(`[SUKSES TOTAL] Status untuk Rp ${keyTransaksi} diubah menjadi SUCCESS!`); | |
| } else { | |
| // Jika tidak cocok, kita cek apakah ada spasi atau kecocokan parsial | |
| console.log(`[BELUM COCOK] Angka "${keyTransaksi}" didapat, tapi tidak ada di antrean web.`); | |
| console.log(`Daftar antrean aktif saat ini:`, Object.keys(databaseTransaksi)); | |
| } | |
| } else { | |
| console.log(`[EROR PARSING] Tidak ditemukan karakter angka di bagian depan notifikasi.`); | |
| } | |
| } catch (error) { | |
| console.error("Gagal total memproses ekstraksi teks:", error); | |
| } | |
| } | |
| // Tetap kirim 200 ke MacroDroid agar tidak dianggap eror oleh HP | |
| return res.status(200).json({ status: 'success' }); | |
| }); | |
| // 5. FRONTEND HALAMAN UTAMA (Dengan tombol Manual Cek dan Cancel) | |
| app.get('/', (req, res) => { | |
| res.send(` | |
| <!DOCTYPE html> | |
| <html lang="id"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FAREL SHOP - Kasir QRIS</title> | |
| <script src="https://cdn.jsdelivr.net/npm/qrcode@1.4.4/build/qrcode.min.js"></script> | |
| <style> | |
| body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f4f6f9; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: center; min-height: 100vh; } | |
| .card { background: white; padding: 30px; border-radius: 16px; box-shadow: 0 10px 25px rgba(0,0,0,0.05); width: 100%; max-width: 400px; text-align: center; } | |
| h2 { color: #111; margin-bottom: 5px; } | |
| p { color: #666; font-size: 14px; margin-top: 0; } | |
| input { width: 100%; padding: 12px; margin: 15px 0; border: 1px solid #ddd; border-radius: 8px; box-sizing: border-box; font-size: 16px; text-align: center; } | |
| button { width: 100%; background: #10b981; color: white; border: none; padding: 12px; border-radius: 8px; font-size: 16px; font-weight: bold; cursor: pointer; transition: 0.2s; margin-bottom: 10px; } | |
| button:hover { background: #059669; } | |
| .btn-manual { background: #3b82f6; } | |
| .btn-manual:hover { background: #2563eb; } | |
| .btn-cancel { background: #ef4444; } | |
| .btn-cancel:hover { background: #dc2626; } | |
| #qrcode { margin: 20px auto; background: #fff; padding: 10px; border: 1px solid #eee; display: inline-block; } | |
| .status-box { margin-top: 15px; padding: 10px; border-radius: 8px; font-weight: bold; display: none; } | |
| .pending { background: #fef3c7; color: #d97706; } | |
| .success { background: #d1fae5; color: #059669; animation: pulse 1.5s infinite; } | |
| @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.03); } 100% { transform: scale(1); } } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="card"> | |
| <h2>FAREL SHOP</h2> | |
| <p>Generate QRIS & Cek Otomatis</p> | |
| <input type="number" id="nominalInput" placeholder="Masukkan Nominal (Contoh: 15000)" /> | |
| <button onclick="buatQRIS()">Buat Kode QRIS</button> | |
| <div id="qrisSection" style="display:none; margin-top: 20px;"> | |
| <h3 id="displayNominal"></h3> | |
| <canvas id="qrcode"></canvas> | |
| <div id="statusLabel" class="status-box">Menunggu Pembayaran...</div> | |
| <div style="margin-top: 15px;"> | |
| <button class="btn-manual" onclick="cekStatusManual()">π Cek Status Manual</button> | |
| <button class="btn-cancel" onclick="batalkanTransaksi()">β Batalkan Transaksi</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let intervalCekStatus; | |
| let currentNominal = ""; | |
| async function buatQRIS() { | |
| clearInterval(intervalCekStatus); | |
| const nominal = document.getElementById('nominalInput').value; | |
| if(!nominal) return alert('Masukkan nominal terlebih dahulu!'); | |
| currentNominal = parseInt(nominal, 10).toString(); | |
| const res = await fetch('/create?nominal=' + currentNominal); | |
| const data = await res.json(); | |
| if(data.status === 'success') { | |
| document.getElementById('qrisSection').style.display = 'block'; | |
| document.getElementById('displayNominal').innerText = 'Total: Rp ' + parseInt(currentNominal).toLocaleString('id-ID'); | |
| QRCode.toCanvas(document.getElementById('qrcode'), data.qris_string, { width: 250 }, function (error) { | |
| if (error) console.error(error); | |
| }); | |
| const statusLabel = document.getElementById('statusLabel'); | |
| statusLabel.style.display = 'block'; | |
| statusLabel.className = 'status-box pending'; | |
| statusLabel.innerText = 'Menunggu Pembayaran...'; | |
| // Tetap berjalan real-time mengecek otomatis tiap 2 detik | |
| intervalCekStatus = setInterval(() => { | |
| cekStatusPembayaranOtomatis(currentNominal); | |
| }, 2000); | |
| } | |
| } | |
| async function cekStatusPembayaranOtomatis(nominal) { | |
| const res = await fetch('/check-status?nominal=' + nominal); | |
| const data = await res.json(); | |
| if(data.status === 'SUCCESS') { | |
| ubahKeTampilanSukses(nominal); | |
| } | |
| } | |
| async function cekStatusManual() { | |
| if(!currentNominal) return; | |
| const res = await fetch('/check-status?nominal=' + currentNominal); | |
| const data = await res.json(); | |
| if(data.status === 'SUCCESS') { | |
| ubahKeTampilanSukses(currentNominal); | |
| } else { | |
| alert('Status transaksi Rp ' + parseInt(currentNominal).toLocaleString('id-ID') + ' masih BELUM DIBAYAR.'); | |
| } | |
| } | |
| async function batalkanTransaksi() { | |
| if(!currentNominal) return; | |
| if(confirm('Apakah Anda yakin ingin membatalkan transaksi ini?')) { | |
| clearInterval(intervalCekStatus); | |
| await fetch('/cancel?nominal=' + currentNominal); | |
| document.getElementById('qrisSection').style.display = 'none'; | |
| document.getElementById('nominalInput').value = ''; | |
| currentNominal = ""; | |
| alert('Transaksi berhasil dibatalkan.'); | |
| } | |
| } | |
| function ubahKeTampilanSukses(nominal) { | |
| clearInterval(intervalCekStatus); | |
| const statusLabel = document.getElementById('statusLabel'); | |
| statusLabel.className = 'status-box success'; | |
| statusLabel.innerText = 'π PEMBAYARAN BERHASIL!'; | |
| alert('Transaksi Rp ' + parseInt(nominal).toLocaleString('id-ID') + ' Sukses Diterima!'); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| `); | |
| }); | |
| const PORT = process.env.PORT || 7860; | |
| app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); |