UNPKG

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