payment / index.js
FarelDeveloper's picture
Update index.js
cc8d479 verified
Raw
History Blame Contribute Delete
11.8 kB
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}`));