File size: 11,782 Bytes
7619d73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d630ea1
 
7619d73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d630ea1
7619d73
 
 
 
 
 
 
d630ea1
7619d73
 
 
 
d630ea1
7619d73
d630ea1
 
 
7619d73
 
 
 
 
 
d630ea1
 
 
 
 
 
 
 
 
 
 
 
cc8d479
7619d73
 
cc8d479
7619d73
cc8d479
7619d73
cc8d479
 
 
 
 
7619d73
cc8d479
 
 
 
 
 
d630ea1
 
 
cc8d479
a067d79
cc8d479
 
 
7619d73
cc8d479
 
7619d73
 
cc8d479
7619d73
 
cc8d479
 
7619d73
 
 
d630ea1
7619d73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d630ea1
7619d73
d630ea1
 
 
 
 
7619d73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d630ea1
 
 
 
 
7619d73
 
 
 
 
d630ea1
7619d73
 
 
 
 
d630ea1
 
7619d73
d630ea1
7619d73
 
 
 
d630ea1
7619d73
 
 
 
 
 
 
 
 
 
d630ea1
7619d73
d630ea1
7619d73
 
 
 
d630ea1
7619d73
 
 
 
d630ea1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7619d73
d630ea1
 
 
 
 
7619d73
 
d630ea1
 
 
 
 
 
 
 
7619d73
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
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}`));