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 @@
-
+
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