autoft-qris
Version:
Package untuk generate QRIS dengan 2 tema (Biru & Hijau) dan cek payment status secara realtime dengan API OrderKuota
175 lines (153 loc) • 6.65 kB
JavaScript
const QRCode = require('qrcode');
const { createCanvas, loadImage } = require('canvas');
const fs = require('fs');
function drawRoundedRect(ctx, x, y, width, height, radius) {
const r = Math.min(radius, width / 2, height / 2);
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + width, y, x + width, y + height, r);
ctx.arcTo(x + width, y + height, x, y + height, r);
ctx.arcTo(x, y + height, x, y, r);
ctx.arcTo(x, y, x + width, y, r);
ctx.closePath();
}
class QRISGenerator {
constructor(config) {
if (!config.baseQrString) {
throw new Error('baseQrString harus diisi');
}
this.config = {
baseQrString: config.baseQrString,
logoPath: config.logoPath
};
}
async generateQRWithLogo(qrString) {
try {
if (!qrString) {
throw new Error('qrString tidak boleh kosong');
}
const size = 500;
const canvas = createCanvas(size, size);
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, size, size);
ctx.fillStyle = '#ffffff';
drawRoundedRect(ctx, 0, 0, size, size, 36);
ctx.fill();
const qr = QRCode.create(qrString, { errorCorrectionLevel: 'H' });
const moduleCount = qr.modules.size;
const quietZoneModules = 4;
const moduleSize = Math.floor(size / (moduleCount + quietZoneModules * 2));
const qrPixelSize = moduleSize * moduleCount;
const margin = Math.floor((size - qrPixelSize) / 2);
const inFinder = (x, y) => {
const topLeft = x < 7 && y < 7;
const topRight = x >= moduleCount - 7 && y < 7;
const bottomLeft = x < 7 && y >= moduleCount - 7;
return topLeft || topRight || bottomLeft;
};
ctx.fillStyle = '#0A0A0A';
const dotRadius = moduleSize * 0.38;
for (let y = 0; y < moduleCount; y++) {
for (let x = 0; x < moduleCount; x++) {
if (inFinder(x, y)) continue;
const isDark = qr.modules.data[y * moduleCount + x];
if (!isDark) continue;
const cx = margin + (x + 0.5) * moduleSize;
const cy = margin + (y + 0.5) * moduleSize;
ctx.beginPath();
ctx.arc(cx, cy, dotRadius, 0, Math.PI * 2);
ctx.fill();
}
}
const cornerColor = '#2563EB';
const innerCornerColor = '#1E3A8A';
const drawFinder = (gridX, gridY) => {
const x = margin + gridX * moduleSize;
const y = margin + gridY * moduleSize;
const outerSize = moduleSize * 7;
const midSize = moduleSize * 5;
const innerSize = moduleSize * 3;
const rOuter = moduleSize * 0.6;
const rMid = moduleSize * 0.5;
const rInner = moduleSize * 0.4;
ctx.fillStyle = cornerColor;
drawRoundedRect(ctx, x, y, outerSize, outerSize, rOuter);
ctx.fill();
ctx.fillStyle = '#ffffff';
drawRoundedRect(
ctx,
x + (outerSize - midSize) / 2,
y + (outerSize - midSize) / 2,
midSize,
midSize,
rMid
);
ctx.fill();
ctx.fillStyle = innerCornerColor;
drawRoundedRect(
ctx,
x + (outerSize - innerSize) / 2,
y + (outerSize - innerSize) / 2,
innerSize,
innerSize,
rInner
);
ctx.fill();
};
drawFinder(0, 0);
drawFinder(moduleCount - 7, 0);
drawFinder(0, moduleCount - 7);
if (this.config.logoPath && fs.existsSync(this.config.logoPath)) {
const logo = await loadImage(this.config.logoPath);
const logoSize = size * 0.22;
const logoPosition = (size - logoSize) / 2;
ctx.fillStyle = '#FFFFFF';
drawRoundedRect(ctx, logoPosition - 6, logoPosition - 6, logoSize + 12, logoSize + 12, 12);
ctx.fill();
ctx.drawImage(logo, logoPosition, logoPosition, logoSize, logoSize);
}
return canvas.toBuffer('image/png');
} catch (error) {
throw new Error('Gagal generate QR: ' + error.message);
}
}
generateQrString(amount) {
try {
if (!amount || amount <= 0) {
throw new Error('Nominal harus lebih besar dari 0');
}
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;