From 34d4563a5f711c1f95c084bec20afa9c84b561eb Mon Sep 17 00:00:00 2001 From: oxpoe Date: Thu, 5 Nov 2026 12:48:59 +0700 Subject: [PATCH] fix(qris): kode unik lintas order + pelunasan multi-tagihan; fix kirim tagihan manual - webhook QRIS: nominal total tagihan + kode -> lunaskan semua tagihan pelanggan - isQrisAmountTaken() cek invoice+voucher+donasi (cegah ambigu) - billingSendService: require('qrcode'), nominal unik + {{qris_kode}}, fallback link - pilih tagihan terlama; pesan pengingat manual yang jujur saat di-skip - versi 0.5.5 --- CHANGELOG.md | 9 +++++++++ app-customer.js | 16 +++++++++++++++- package.json | 2 +- routes/adminPortal.js | 21 +++++++++++++-------- routes/customerAPI.js | 8 ++++---- routes/customerPortal.js | 15 +++++---------- services/billingSendService.js | 31 ++++++++++++++++--------------- services/cronService.js | 16 ++++++++-------- services/invoiceQrisService.js | 28 +++++++++++++++++++++++++--- version.txt | 2 +- 10 files changed, 97 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 253f5dd..a711c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/). +## [0.5.5] — 2026-11-05 — "qris-fix" + +### Diperbaiki +- **QRIS multi-tagihan**: pembayaran dengan nominal total beberapa tagihan kini melunaskan **semua** tagihan pelanggan (sebelumnya hanya invoice yang menyimpan kode unik, sisanya tetap menunggak). +- **Kode unik bentrok**: pengecekan nominal unik sekarang mencakup invoice, voucher, dan donasi sekaligus — mencegah pembayaran dianggap ambigu oleh webhook (dulu bisa tidak diproses sama sekali). +- **Kirim tagihan QRIS manual**: gambar QR benar-benar terlampir (path `qrcode` tidak lagi hardcoded ke `/opt/billing-rtrw`), nominal di pesan memakai nominal unik, dan placeholder `{{qris_kode}}` didukung. +- **Kirim tagihan manual** memilih tagihan paling lama lebih dulu (sebelumnya tagihan terbaru). +- Pesan **pengingat manual** kini jujur saat tidak dikirim (WA/notifikasi OFF, gateway belum siap, atau tidak ada target hari ini), bukan “dijalankan: target 0”. + ## [0.5.4] — 2026-11-05 — "wa-notice" ### Ditambahkan diff --git a/app-customer.js b/app-customer.js index 465fe86..cc8b359 100644 --- a/app-customer.js +++ b/app-customer.js @@ -1011,7 +1011,21 @@ app.post('/api/webhook/v1/payment-notif', multer().any(), async (req, res) => { const custId = Number(inv.customer_id || 0); if (invId > 0) { const noteLine = `AUTO-QRIS: cocok nominal unik Rp ${amount} (service=${String(service || '-')}, notif=${notifId || '-'})`; - markInvoicePaidAppendNote.run('QRIS', noteLine, noteLine, notifId || null, invId); + // QRIS multi-tagihan: nominal = total semua tagihan + kode unik yang disimpan + // di salah satu invoice. Lunaskan semuanya, jangan hanya invoice pemilik kode. + const unpaidRows = custId > 0 + ? db.prepare("SELECT id, amount FROM invoices WHERE customer_id=? AND status='unpaid' ORDER BY period_year ASC, period_month ASC, id ASC").all(custId) + : []; + const totalUnpaid = unpaidRows.reduce((sum, row) => sum + (Number(row.amount) || 0), 0); + const qrisCode = Number(inv.qris_unique_code || 0) || 0; + if (qrisCode > 0 && unpaidRows.length > 1 && totalUnpaid + qrisCode === Number(amount)) { + const noteBundle = `${noteLine} — lunas ${unpaidRows.length} tagihan`; + const payOne = db.prepare(`UPDATE invoices SET status='paid', paid_at=NOW_LOCAL(), paid_by_name=?, notes=CASE WHEN notes IS NULL OR TRIM(notes) = '' THEN ? ELSE notes || '\n' || ? END, qris_paid_notif_id=? WHERE id=? AND status='unpaid'`); + for (const row of unpaidRows) payOne.run('QRIS', noteBundle, noteBundle, notifId || null, row.id); + logger.info(`[WEBHOOK][payment-notif] MATCH bundle invoice=${invId} invoices=${unpaidRows.length} total=${totalUnpaid} amount=${amount}`); + } else { + markInvoicePaidAppendNote.run('QRIS', noteLine, noteLine, notifId || null, invId); + } matchedInvoiceId = invId; if (notifId) { diff --git a/package.json b/package.json index a219331..2872752 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-isp", - "version": "0.5.4", + "version": "0.5.5", "description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)", "main": "app-customer.js", "scripts": { diff --git a/routes/adminPortal.js b/routes/adminPortal.js index 5a7cfcd..4f1bb07 100644 --- a/routes/adminPortal.js +++ b/routes/adminPortal.js @@ -72,6 +72,7 @@ const _jimpMod = require('jimp'); const Jimp = _jimpMod.Jimp || _jimpMod; const jsQR = require('jsqr'); const qrisUtil = require('../utils/qrisUtil'); +const qrisUniq = require('../services/invoiceQrisService'); const { MultiFormatReader, BarcodeFormat, DecodeHintType, BinaryBitmap, HybridBinarizer, RGBLuminanceSource } = require('@zxing/library'); const QRCode = require('qrcode'); const acsPortal = require('./acsPortal'); @@ -3105,7 +3106,6 @@ router.post('/billing/:id/qris-assign', requireAdminSession, (req, res) => { const baseAmount = Number(inv.amount || 0); if (!Number.isFinite(baseAmount) || baseAmount <= 0) throw new Error('Nominal tagihan tidak valid'); - const exists = db.prepare('SELECT id FROM invoices WHERE status=? AND qris_amount_unique=? AND id!=? LIMIT 1'); const update = db.prepare(` UPDATE invoices SET qris_unique_code=?, qris_amount_unique=?, qris_assigned_at=(NOW_LOCAL()) @@ -3118,7 +3118,7 @@ router.post('/billing/:id/qris-assign', requireAdminSession, (req, res) => { for (let i = 0; i < 50; i++) { const code = 1 + Math.floor(Math.random() * 999); const amount = baseAmount + code; - if (!exists.get('unpaid', amount, invId)) { + if (!qrisUniq.isQrisAmountTaken(amount, { excludeInvoiceId: invId })) { chosenCode = code; chosenAmount = amount; break; @@ -3128,7 +3128,7 @@ router.post('/billing/:id/qris-assign', requireAdminSession, (req, res) => { if (!chosenAmount) { for (let code = 1; code <= 999; code++) { const amount = baseAmount + code; - if (!exists.get('unpaid', amount, invId)) { + if (!qrisUniq.isQrisAmountTaken(amount, { excludeInvoiceId: invId })) { chosenCode = code; chosenAmount = amount; break; @@ -3202,7 +3202,6 @@ router.post('/billing/:id/whatsapp', requireAdminSession, async (req, res) => { if (hasStaticQris && String(inv.status) === 'unpaid' && (!qrisAmountUnique || !qrisCode || (qrisAmountUnique - qrisCode !== baseAmount))) { const invId = Number(inv.id); if (Number.isFinite(invId) && invId > 0 && Number.isFinite(baseAmount) && baseAmount > 0) { - const exists = db.prepare('SELECT id FROM invoices WHERE status=? AND qris_amount_unique=? AND id!=? LIMIT 1'); const update = db.prepare(` UPDATE invoices SET qris_unique_code=?, qris_amount_unique=?, qris_assigned_at=(NOW_LOCAL()) @@ -3214,7 +3213,7 @@ router.post('/billing/:id/whatsapp', requireAdminSession, async (req, res) => { if (chosenCode > 0) { const pAmt = baseAmount + chosenCode; - if (!exists.get('unpaid', pAmt, invId)) { + if (!qrisUniq.isQrisAmountTaken(pAmt, { excludeInvoiceId: invId })) { chosenAmount = pAmt; } } @@ -3223,7 +3222,7 @@ router.post('/billing/:id/whatsapp', requireAdminSession, async (req, res) => { for (let i = 0; i < 50; i++) { const code = 1 + Math.floor(Math.random() * 999); const amount = baseAmount + code; - if (!exists.get('unpaid', amount, invId)) { + if (!qrisUniq.isQrisAmountTaken(amount, { excludeInvoiceId: invId })) { chosenCode = code; chosenAmount = amount; break; @@ -3233,7 +3232,7 @@ router.post('/billing/:id/whatsapp', requireAdminSession, async (req, res) => { if (!chosenAmount) { for (let code = 1; code <= 999; code++) { const amount = baseAmount + code; - if (!exists.get('unpaid', amount, invId)) { + if (!qrisUniq.isQrisAmountTaken(amount, { excludeInvoiceId: invId })) { chosenCode = code; chosenAmount = amount; break; @@ -7235,8 +7234,14 @@ router.post('/whatsapp/auto-billing', requireAdminSession, express.urlencoded({ router.post('/whatsapp/run-reminder-now', requireAdminSession, async (req, res) => { try { const r = await require('../services/cronService').runAutoBillingReminder(); + const skipLabels = { + disabled: 'pengingat otomatis sedang OFF', + wa_disabled: 'WhatsApp atau notifikasi tagihan sedang OFF', + gateway: 'gateway WhatsApp belum siap', + no_targets: 'tidak ada pelanggan pada jadwal pengingat hari ini' + }; const msg = (r && r.skipped) - ? ('Pengingat dilewati (' + r.skipped + '). Pastikan opsi otomatis & WA aktif.') + ? ('Pengingat tidak dikirim: ' + (skipLabels[r.skipped] || r.skipped) + '.') : ('Pengingat dijalankan: target ' + ((r && r.target) || 0) + ', terkirim ' + ((r && r.sent) || 0) + ', gagal ' + ((r && r.failed) || 0) + '.'); return res.json({ ok: true, message: msg, data: r || null }); } catch (e) { diff --git a/routes/customerAPI.js b/routes/customerAPI.js index 57a0e8c..1f59f16 100644 --- a/routes/customerAPI.js +++ b/routes/customerAPI.js @@ -18,6 +18,7 @@ const adminSvc = require('../services/adminService'); const techSvc = require('../services/techService'); const mikrotikService = require('../services/mikrotikService'); const qrisUtil = require('../utils/qrisUtil'); +const qrisUniq = require('../services/invoiceQrisService'); const QRCode = require('qrcode'); // Helper Token JWT / HMAC @@ -3675,7 +3676,6 @@ function ensureCustomerApiInvoiceQrisUnique(inv) { return { uniqueCode: currentCode, amountUnique: currentAmount }; } - const exists = db.prepare('SELECT id FROM invoices WHERE status=? AND qris_amount_unique=? AND id!=? LIMIT 1'); let chosenCode = 0; let chosenAmount = 0; @@ -3683,7 +3683,7 @@ function ensureCustomerApiInvoiceQrisUnique(inv) { if (custId > 0) { const prefCode = (custId % 499 === 0) ? 499 : (custId % 499); const prefAmount = baseAmount + prefCode; - if (!exists.get('unpaid', prefAmount, invId)) { + if (!qrisUniq.isQrisAmountTaken(prefAmount, { excludeInvoiceId: invId })) { chosenCode = prefCode; chosenAmount = prefAmount; } @@ -3693,7 +3693,7 @@ function ensureCustomerApiInvoiceQrisUnique(inv) { if (!chosenAmount) { for (let code = 1; code <= 499; code++) { const amount = baseAmount + code; - if (!exists.get('unpaid', amount, invId)) { + if (!qrisUniq.isQrisAmountTaken(amount, { excludeInvoiceId: invId })) { chosenCode = code; chosenAmount = amount; break; @@ -3705,7 +3705,7 @@ function ensureCustomerApiInvoiceQrisUnique(inv) { if (!chosenAmount) { for (let code = 500; code <= 999; code++) { const amount = baseAmount + code; - if (!exists.get('unpaid', amount, invId)) { + if (!qrisUniq.isQrisAmountTaken(amount, { excludeInvoiceId: invId })) { chosenCode = code; chosenAmount = amount; break; diff --git a/routes/customerPortal.js b/routes/customerPortal.js index a6f9cb4..382f197 100644 --- a/routes/customerPortal.js +++ b/routes/customerPortal.js @@ -26,6 +26,7 @@ const QRCode = require('qrcode'); const _jimpMod = require('jimp'); const Jimp = _jimpMod.Jimp || _jimpMod; const qrisUtil = require('../utils/qrisUtil'); +const qrisUniq = require('../services/invoiceQrisService'); const whatsappService = require('../services/whatsappService'); const { BinaryBitmap, HybridBinarizer, RGBLuminanceSource, MultiFormatReader, BarcodeFormat, DecodeHintType } = require('@zxing/library'); let loginRateLimiter = (req, res, next) => next(); @@ -437,16 +438,10 @@ function getBaseUrl(req, settings) { function isQrisAmountAvailable(amount, opts = {}) { const amt = Number(amount || 0); if (!Number.isFinite(amt) || amt <= 0) return false; - const excludeInvoiceId = Number(opts.excludeInvoiceId || 0); - const excludeVoucherOrderId = Number(opts.excludeVoucherOrderId || 0); - - const inv = db.prepare('SELECT id FROM invoices WHERE status=? AND qris_amount_unique=? AND id!=? LIMIT 1').get('unpaid', amt, excludeInvoiceId); - if (inv && inv.id) return false; - - const ord = db.prepare('SELECT id FROM public_voucher_orders WHERE status=? AND qris_amount_unique=? AND id!=? LIMIT 1').get('pending', amt, excludeVoucherOrderId); - if (ord && ord.id) return false; - - return true; + return !qrisUniq.isQrisAmountTaken(amt, { + excludeInvoiceId: opts.excludeInvoiceId, + excludeVoucherOrderId: opts.excludeVoucherOrderId + }); } router.get('/qris/static.jpg', async (req, res) => { diff --git a/services/billingSendService.js b/services/billingSendService.js index f903d6f..8f86876 100644 --- a/services/billingSendService.js +++ b/services/billingSendService.js @@ -40,15 +40,19 @@ function spintax(t) { }); } -function buildMessage(tplRaw, c, inv) { +function buildMessage(tplRaw, c, inv, qr) { const amount = Number(inv.amount || 0); + const qrisAmount = Number((qr && qr.amount) || 0) || amount; + const qrisCode = Number((qr && qr.uniqueCode) || 0) || 0; + const qrisAttached = !!(qr && qr.payload); const period = inv.period_month + '/' + inv.period_year; return spintax(String(tplRaw || '') .replace(/{{id_pelanggan}}/gi, c.customer_code || ('ID:' + c.id)) .replace(/{{nama}}/gi, c.name || 'Pelanggan') .replace(/{{paket}}/gi, inv.package_name || '-') .replace(/{{tagihan}}/gi, amount.toLocaleString('id-ID')) - .replace(/{{qris_nominal}}/gi, amount.toLocaleString('id-ID')) + .replace(/{{qris_nominal}}/gi, qrisAmount.toLocaleString('id-ID')) + .replace(/{{qris_kode}}/gi, String(qrisCode).padStart(3, '0')) .replace(/{{rincian}}/gi, period) .replace(/{{periode}}/gi, period) .replace(/{{bulan_nunggak}}/gi, String(c.unpaid_streak || 0)) @@ -57,7 +61,7 @@ function buildMessage(tplRaw, c, inv) { .replace(/{{link_portal}}/gi, portalLink()) .replace(/{{company}}/gi, getSetting('company_header', '') || '-') .replace(/{{company_phone}}/gi, getSetting('company_phone', '') || '-') - .replace(/{{qris_qr}}/gi, 'QRIS terlampir')); + .replace(/{{qris_qr}}/gi, qrisAttached ? 'QRIS terlampir' : ('Portal Pelanggan: ' + portalLink()))); } function buildTargets(isolateDay) { @@ -72,7 +76,8 @@ function buildTargets(isolateDay) { try { invs = billingSvc.getUnpaidInvoicesByCustomerId(c.id) || []; } catch (e) {} if (!invs.length) continue; if (!c.phone || String(c.phone).replace(/\D/g, '').length < 9) continue; - targets.push({ c, inv: invs[invs.length - 1] }); + // invoice terurut dari periode terlama; tagih yang paling lama dulu + targets.push({ c, inv: invs[0] }); } return targets; } @@ -108,23 +113,19 @@ async function start(opts = {}) { const waNotify = require('./waNotifyService'); let QRCode = null, ensureInvoiceQris = null; - if (isQris) { try { QRCode = require('/opt/billing-rtrw/node_modules/qrcode'); } catch (e) {} try { ensureInvoiceQris = require('./invoiceQrisService').ensureInvoiceQris; } catch (e) {} } + if (isQris) { try { QRCode = require('qrcode'); } catch (e) {} try { ensureInvoiceQris = require('./invoiceQrisService').ensureInvoiceQris; } catch (e) {} } (async () => { for (let i = 0; i < targets.length; i++) { if (STATUS.stopped) break; const { c, inv } = targets[i]; try { - const caption = buildMessage(tplRaw, c, inv); - if (isQris && QRCode && ensureInvoiceQris) { - const qr = ensureInvoiceQris(inv) || {}; - if (qr.payload) { - const url = await QRCode.toDataURL(qr.payload, { errorCorrectionLevel: 'M', margin: 1, width: 520 }); - const buf = Buffer.from(String(url).split(',')[1], 'base64'); - await waNotify.sendImage(c.phone, buf, caption); - } else { - await waNotify.sendText(c.phone, caption); - } + const qr = (isQris && ensureInvoiceQris) ? (ensureInvoiceQris(inv) || {}) : null; + const caption = buildMessage(tplRaw, c, inv, qr); + if (isQris && QRCode && qr && qr.payload) { + const url = await QRCode.toDataURL(qr.payload, { errorCorrectionLevel: 'M', margin: 1, width: 520 }); + const buf = Buffer.from(String(url).split(',')[1], 'base64'); + await waNotify.sendImage(c.phone, buf, caption); } else { await waNotify.sendText(c.phone, caption); } diff --git a/services/cronService.js b/services/cronService.js index 3122b31..11d2ba7 100644 --- a/services/cronService.js +++ b/services/cronService.js @@ -11,6 +11,7 @@ const usageSvc = require('./usageService'); const { getSetting } = require('../config/settingsManager'); const db = require('../config/database'); const qrisUtil = require('../utils/qrisUtil'); +const qrisUniq = require('./invoiceQrisService'); const { formatUnpaidInvoicesSummary } = require('./whatsappService'); // Helper: Random delay generator untuk smart rate limiting @@ -60,7 +61,7 @@ async function runAutoBillingReminder() { const enabled = getSetting('whatsapp_auto_billing_enabled', false); const waEnabled = getSetting('whatsapp_enabled', false); const billingEnabled = getSetting('whatsapp_billing_to_customer_enabled', true); - if (!enabled || !waEnabled || !billingEnabled) return { target: 0, sent: 0, failed: 0, skipped: 'disabled' }; + if (!enabled || !waEnabled || !billingEnabled) return { target: 0, sent: 0, failed: 0, skipped: (!waEnabled || !billingEnabled) ? 'wa_disabled' : 'disabled' }; const gatewayType = getSetting('wa_gateway_type', 'gowa'); const waSvc = require('./whatsappService'); @@ -70,13 +71,13 @@ async function runAutoBillingReminder() { const token = getSetting('meta_access_token', ''); if (!phoneId || !token) { logger.warn('[CRON] Kredensial Meta API belum diisi, pengingat tagihan otomatis dilewati.'); - return; + return { target: 0, sent: 0, failed: 0, skipped: 'gateway' }; } } else if (gatewayType === 'fonnte') { const token = getSetting('fonnte_token', ''); if (!token) { logger.warn('[CRON] Token Fonnte belum diisi, pengingat tagihan otomatis dilewati.'); - return; + return { target: 0, sent: 0, failed: 0, skipped: 'gateway' }; } } else { // Gateway gowa/baileys/lainnya → cek koneksi gateway aktif (gateway-aware) @@ -84,7 +85,7 @@ async function runAutoBillingReminder() { const connected = await waNotify.isConnected().catch(() => false); if (!connected) { logger.warn('[CRON] WhatsApp gateway belum terhubung, pengingat tagihan otomatis dilewati.'); - return; + return { target: 0, sent: 0, failed: 0, skipped: 'gateway' }; } } @@ -213,14 +214,13 @@ async function runAutoBillingReminder() { // Jika amt tidak ada atau amt tidak cocok dengan akumulasi total tagihan + kode, buat/perbarui nominal unik if ((!code || !amt || (amt - code !== baseAmount)) && invId > 0 && baseAmount > 0) { - const exists = db.prepare('SELECT id FROM invoices WHERE status=? AND qris_amount_unique=? AND id!=? LIMIT 1'); const custId = Number(c?.id || inv?.customer_id || 0); let tryCode = 0; let tryAmt = 0; if (code > 0) { const pAmt = baseAmount + code; - if (!exists.get('unpaid', pAmt, invId)) { + if (!qrisUniq.isQrisAmountTaken(pAmt, { excludeInvoiceId: invId })) { tryCode = code; tryAmt = pAmt; } @@ -229,7 +229,7 @@ async function runAutoBillingReminder() { if (!tryAmt && custId > 0) { const prefCode = (custId % 499 === 0) ? 499 : (custId % 499); const pAmt = baseAmount + prefCode; - if (!exists.get('unpaid', pAmt, invId)) { + if (!qrisUniq.isQrisAmountTaken(pAmt, { excludeInvoiceId: invId })) { tryCode = prefCode; tryAmt = pAmt; } @@ -238,7 +238,7 @@ async function runAutoBillingReminder() { if (!tryAmt) { for (let randCode = 1; randCode <= 499; randCode++) { const pAmt = baseAmount + randCode; - if (!exists.get('unpaid', pAmt, invId)) { + if (!qrisUniq.isQrisAmountTaken(pAmt, { excludeInvoiceId: invId })) { tryCode = randCode; tryAmt = pAmt; break; diff --git a/services/invoiceQrisService.js b/services/invoiceQrisService.js index 06a9dee..b135a77 100644 --- a/services/invoiceQrisService.js +++ b/services/invoiceQrisService.js @@ -7,6 +7,29 @@ const db = require('../config/database'); const qrisUtil = require('../utils/qrisUtil'); const { getSettings } = require('../config/settingsManager'); +/** + * Cek apakah nominal unik sudah dipakai order lain yang masih menunggu bayar. + * WAJIB mencakup invoice, voucher, dan donasi: webhook mencocokkan ketiganya + * sekaligus, jadi nominal yang dipakai dua order berbeda akan dianggap ambigu + * dan pembayarannya tidak diproses. + */ +function isQrisAmountTaken(amount, opts = {}) { + const amt = Number(amount || 0); + if (!Number.isFinite(amt) || amt <= 0) return false; + const exInv = Number(opts.excludeInvoiceId || 0); + const exVoucher = Number(opts.excludeVoucherOrderId || 0); + const exDonation = Number(opts.excludeDonationOrderId || 0); + try { + const inv = db.prepare('SELECT id FROM invoices WHERE status=? AND qris_amount_unique=? AND id!=? LIMIT 1').get('unpaid', amt, exInv); + if (inv && inv.id) return true; + const voc = db.prepare('SELECT id FROM public_voucher_orders WHERE status=? AND qris_amount_unique=? AND id!=? LIMIT 1').get('pending', amt, exVoucher); + if (voc && voc.id) return true; + const don = db.prepare('SELECT id FROM public_donation_orders WHERE status=? AND qris_amount_unique=? AND id!=? LIMIT 1').get('pending', amt, exDonation); + if (don && don.id) return true; + } catch (e) { /* tabel opsional belum ada */ } + return false; +} + function ensureInvoiceQris(inv) { const settings = getSettings() || {}; const invId = Number((inv && inv.id) || 0); @@ -17,11 +40,10 @@ function ensureInvoiceQris(inv) { let amountUnique = Number(inv.qris_amount_unique || 0); if (!(amountUnique > 0 && code > 0 && (amountUnique - code) === base)) { - const exists = db.prepare('SELECT id FROM invoices WHERE status=? AND qris_amount_unique=? AND id!=? LIMIT 1'); let chosen = 0, chosenAmt = 0; for (let c = 1; c <= 999; c++) { const a = base + c; - if (!exists.get('unpaid', a, invId)) { chosen = c; chosenAmt = a; break; } + if (!isQrisAmountTaken(a, { excludeInvoiceId: invId })) { chosen = c; chosenAmt = a; break; } } if (chosen) { code = chosen; @@ -43,4 +65,4 @@ function ensureInvoiceQris(inv) { return { uniqueCode: code, amount: finalAmount, payload }; } -module.exports = { ensureInvoiceQris }; +module.exports = { ensureInvoiceQris, isQrisAmountTaken }; diff --git a/version.txt b/version.txt index 167b000..389facc 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.5.4 \ No newline at end of file +0.5.5 \ No newline at end of file