UNPKG

wahdx-api

Version:

Package untuk generate QRIS dan cek payment status secara realtime dengan API OrderKuota dari https://api.wahdx.co

208 lines (182 loc) 7.52 kB
const QRCode = require("qrcode"); const fs = require("fs"); const sharp = require("sharp"); const jsQR = require("jsqr"); const path = require("path"); // CJS sudah menyediakan __filename dan __dirname secara otomatis class QRISGenerator { constructor(config = {}) { this.config = { baseQrString: config.baseQrString || '', defaultQrPath: config.defaultQrPath || 'QRIS.png' }; } /** * Membaca QR code dari gambar dan mengekstrak baseQrString * @param {string} imagePath - Path relatif atau absolut ke file gambar QR * @returns {Promise<string>} - Promise yang menghasilkan baseQrString */ async readQRFromImage(imagePath) { try { // Konversi path relatif ke absolut jika diperlukan const absolutePath = path.isAbsolute(imagePath) ? imagePath : path.resolve(process.cwd(), imagePath); // Periksa apakah file ada if (!fs.existsSync(absolutePath)) { throw new Error(`File tidak ditemukan: ${absolutePath}`); } // Baca gambar menggunakan Sharp const image = sharp(absolutePath); const metadata = await image.metadata(); const { width, height } = metadata; // Konversi ke raw pixel data const rawData = await image .raw() .toBuffer(); // Format data untuk jsQR const imageData = new Uint8ClampedArray(width * height * 4); for (let i = 0; i < rawData.length; i += 3) { // RGB format const pixelIndex = (i / 3) * 4; // Convert RGB index to RGBA index imageData[pixelIndex] = rawData[i]; // R imageData[pixelIndex + 1] = rawData[i + 1]; // G imageData[pixelIndex + 2] = rawData[i + 2]; // B imageData[pixelIndex + 3] = 255; // A (full opacity) } // Dekode QR code menggunakan jsQR const code = jsQR(imageData, width, height); if (code) { // Simpan baseQrString yang dibaca ke config this.config.baseQrString = code.data; return code.data; } else { throw new Error('QR code tidak terdeteksi dalam gambar'); } } catch (error) { console.error('Error saat membaca QR code:', error); throw error; } } /** * Membaca QR code dari file default (biasanya QRIS.png) * @returns {Promise<string>} - Promise yang menghasilkan baseQrString */ async readDefaultQR() { try { // Path default ke file QRIS.png const defaultPath = path.resolve(process.cwd(), this.config.defaultQrPath); return await this.readQRFromImage(defaultPath); } catch (error) { console.error(`Error saat membaca gambar default (${this.config.defaultQrPath}):`, error); throw error; } } /** * Generate QR code sebagai PNG buffer * @param {string} qrString - String QR code yang akan digenerate * @returns {Promise<Buffer>} - Buffer PNG dari QR code */ async generateQRImage(qrString) { try { if (!qrString) { throw new Error('qrString tidak boleh kosong'); } // Gunakan QRCode.toBuffer langsung tanpa canvas const qrBuffer = await QRCode.toBuffer(qrString, { errorCorrectionLevel: 'H', margin: 2, width: 500, color: { dark: '#000000', light: '#ffffff' } }); return qrBuffer; } catch (error) { throw new Error('Gagal generate QR: ' + error.message); } } /** * Membaca QR dari file kemudian generate QR baru dengan nominal * @param {number} amount - Nominal pembayaran * @param {string} qrImagePath - Path ke gambar QR (opsional) * @returns {Promise<{qrString: string, qrBuffer: Buffer}>} */ async generateQRFromImage(amount, qrImagePath = null) { try { // Jika path gambar disediakan, baca dari path tersebut if (qrImagePath) { await this.readQRFromImage(qrImagePath); } // Jika tidak ada path dan baseQrString kosong, gunakan default else if (!this.config.baseQrString) { await this.readDefaultQR(); } // Generate QR string dengan nominal const qrString = this.generateQrString(amount); // Generate QR image const qrBuffer = await this.generateQRImage(qrString); return { qrString, qrBuffer }; } catch (error) { throw new Error('Gagal generate QR dari gambar: ' + error.message); } } /** * Generate QR dengan nominal tertentu * @param {number} amount - Nominal pembayaran * @returns {Promise<{qrString: string, qrBuffer: Buffer}>} */ async generateQR(amount) { const qrString = this.generateQrString(amount); const qrBuffer = await this.generateQRImage(qrString); return { qrString, qrBuffer }; } generateQrString(amount) { try { if (!amount || amount <= 0) { throw new Error('Nominal harus lebih besar dari 0'); } if (!this.config.baseQrString) { throw new Error('BaseQrString tidak tersedia. Gunakan readQRFromImage terlebih dahulu atau berikan baseQrString pada config.'); } if (!this.config.baseQrString.includes("5802ID")) { throw new Error("Format QRIS tidak valid"); } const finalAmount = Math.floor(amount); const qrisBase = this.config.baseQrString.slice(0, -4).replace("010211", "010212"); const nominalStr = finalAmount.toString(); const nominalTag = `54${nominalStr.length.toString().padStart(2, '0')}${nominalStr}`; const insertPosition = qrisBase.indexOf("5802ID"); const qrisWithNominal = qrisBase.slice(0, insertPosition) + nominalTag + qrisBase.slice(insertPosition); const checksum = this.calculateCRC16(qrisWithNominal); return qrisWithNominal + checksum; } catch (error) { throw new Error('Gagal generate string QRIS: ' + error.message); } } calculateCRC16(str) { try { if (!str) { throw new Error('String tidak boleh kosong'); } let crc = 0xFFFF; for (let i = 0; i < str.length; i++) { crc ^= str.charCodeAt(i) << 8; for (let j = 0; j < 8; j++) { crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1); } crc &= 0xFFFF; } return crc.toString(16).toUpperCase().padStart(4, '0'); } catch (error) { throw new Error('Gagal kalkulasi CRC16: ' + error.message); } } } module.exports = QRISGenerator;