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
JavaScript
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;