From d68cc1d664154976d5139d72684a8ecfbc058a42 Mon Sep 17 00:00:00 2001 From: oxpoe Date: Thu, 5 Nov 2026 13:19:14 +0700 Subject: [PATCH] fix(qris): selaras skema hangus, satu tagihan terbaru - batalkan pelunasan multi-tagihan: tagihan lama otomatis void saat tagihan baru terbit - QRIS manual memakai tagihan terbaru, bukan total tunggakan - versi 0.5.6 --- CHANGELOG.md | 10 +++++++++- README.md | 2 +- app-customer.js | 16 +--------------- package.json | 2 +- services/billingSendService.js | 15 +++++++-------- services/invoiceQrisService.js | 5 ++--- version.txt | 2 +- 7 files changed, 22 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a711c58..6b7800b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,18 @@ Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/). +## [0.5.6] — 2026-11-05 — "qris-hangus" + +### Diperbaiki +- **QRIS mengikuti skema hangus**: nominal unik hanya untuk **satu tagihan** (tagihan terbaru). Tagihan lama otomatis `void` saat tagihan baru terbit, jadi tidak ada tunggakan menumpuk. +- Kirim tagihan QRIS manual memakai tagihan **terbaru** (sebelumnya bisa mengambil tagihan lama). + +### Catatan +- Nonaktif otomatis setelah **3 bulan** menunggak dan hapus akun pada bulan berikutnya tetap berjalan sesuai pengaturan `auto_deactivate_months` / `auto_delete_overdue_enabled`. + ## [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). diff --git a/README.md b/README.md index 0cf3c58..4aefb17 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

Lisensi: ISC - Versi 0.5.4 (wa-notice) + Versi 0.5.6 (qris-hangus) Node.js >= 20 Docker Demo diff --git a/app-customer.js b/app-customer.js index cc8b359..465fe86 100644 --- a/app-customer.js +++ b/app-customer.js @@ -1011,21 +1011,7 @@ 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 || '-'})`; - // 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); - } + markInvoicePaidAppendNote.run('QRIS', noteLine, noteLine, notifId || null, invId); matchedInvoiceId = invId; if (notifId) { diff --git a/package.json b/package.json index 2872752..5e6a6cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-isp", - "version": "0.5.5", + "version": "0.5.6", "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/services/billingSendService.js b/services/billingSendService.js index 99baa4c..11ecbec 100644 --- a/services/billingSendService.js +++ b/services/billingSendService.js @@ -40,8 +40,8 @@ function spintax(t) { }); } -function buildMessage(tplRaw, c, inv, qr, totalAmount) { - const amount = Number(totalAmount || inv.amount || 0); +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); @@ -76,9 +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; - // invoice terurut dari periode terlama; tagih yang paling lama dulu - const total = invs.reduce((sum, row) => sum + (Number(row.amount) || 0), 0); - targets.push({ c, inv: invs[0], total }); + // skema hangus: hanya tagihan terbaru yang valid; tagihan lama otomatis void saat tagihan baru terbit + targets.push({ c, inv: invs[invs.length - 1] }); } return targets; } @@ -119,10 +118,10 @@ async function start(opts = {}) { (async () => { for (let i = 0; i < targets.length; i++) { if (STATUS.stopped) break; - const { c, inv, total } = targets[i]; + const { c, inv } = targets[i]; try { - const qr = (isQris && ensureInvoiceQris) ? (ensureInvoiceQris(inv, total) || {}) : null; - const caption = buildMessage(tplRaw, c, inv, qr, total); + 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'); diff --git a/services/invoiceQrisService.js b/services/invoiceQrisService.js index 892b9da..b135a77 100644 --- a/services/invoiceQrisService.js +++ b/services/invoiceQrisService.js @@ -30,11 +30,10 @@ function isQrisAmountTaken(amount, opts = {}) { return false; } -function ensureInvoiceQris(inv, baseAmountOverride) { +function ensureInvoiceQris(inv) { const settings = getSettings() || {}; const invId = Number((inv && inv.id) || 0); - // baseAmountOverride dipakai bila nominal QRIS = total beberapa tagihan - const base = Number(baseAmountOverride || (inv && inv.amount) || 0); + const base = Number((inv && inv.amount) || 0); if (!invId || base <= 0) return { uniqueCode: 0, amount: base, payload: '' }; let code = Number(inv.qris_unique_code || 0); diff --git a/version.txt b/version.txt index 389facc..ad83b1b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.5.5 \ No newline at end of file +0.5.6 \ No newline at end of file