feat(watcher): add BRI QRIS parsing and rules
Implement parsing for BRI QRIS transactions, including direct and forwarded message handling. Update rules to recognize QRIS success messages and adjust spending record mapping accordingly. Enhance tests to cover new functionality.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { parseBriTransfer } from "../parsers/bri.js";
|
||||
import { parseBriQris, parseBriTransfer } from "../parsers/bri.js";
|
||||
import { parseGrabReceipt } from "../parsers/grab.js";
|
||||
import { parseLivinReceipt } from "../parsers/livin.js";
|
||||
import { notifyReceiptIssue } from "../notify/ntfy.js";
|
||||
@@ -7,6 +7,7 @@ const PARSERS = {
|
||||
"livin.qr_payment.success": parseLivinReceipt,
|
||||
"grab.ereceipt.ride": parseGrabReceipt,
|
||||
"bri.transfer.success": parseBriTransfer,
|
||||
"bri.qris.success": parseBriQris,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,3 +66,80 @@ export function parseBriTransfer(body) {
|
||||
details,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse BRImo / Bank BRI QRIS purchase body (plain text).
|
||||
* Required: Nominal (or Total Transaksi) + Nomor Referensi.
|
||||
*/
|
||||
export function parseBriQris(body) {
|
||||
if (!body) return null;
|
||||
|
||||
const payerMatch = body.match(/Halo\s+([^,\n]+)/i);
|
||||
const payerName = payerMatch ? payerMatch[1].trim() : null;
|
||||
|
||||
const tanggalRaw = captureAfterLabel(body, "Tanggal Transaksi");
|
||||
const dateYmd = parseDayMonthYear(tanggalRaw);
|
||||
const timeHms = parseTimeWib(tanggalRaw);
|
||||
const occurredAt = toWibIso(dateYmd, timeHms);
|
||||
|
||||
const nominalRaw = captureAfterLabel(body, "Nominal");
|
||||
let amount = parseIdrAmount(nominalRaw);
|
||||
if (amount == null) {
|
||||
const totalBlock = captureBlockAfterLabel(body, "Total Transaksi", [
|
||||
"Tujuan",
|
||||
"Nama Merchant",
|
||||
"Nomor Referensi",
|
||||
]);
|
||||
amount = parseIdrAmount(totalBlock?.[0] || null);
|
||||
}
|
||||
|
||||
const reference =
|
||||
captureAfterLabel(body, "Nomor Referensi") ||
|
||||
body.match(/Nomor\s+Referensi\s*([^\s\n]+)/i)?.[1] ||
|
||||
null;
|
||||
|
||||
if (amount == null || !reference) return null;
|
||||
|
||||
const merchant = captureAfterLabel(body, "Nama Merchant");
|
||||
const location = captureAfterLabel(body, "Lokasi Merchant");
|
||||
const jenisTransaksi = captureAfterLabel(body, "Jenis Transaksi");
|
||||
const acquirer = captureAfterLabel(body, "Nama Pengakuisisi");
|
||||
const issuer = captureAfterLabel(body, "Nama Penerbit");
|
||||
const customerPan = captureAfterLabel(body, "Kode PAN Pelanggan");
|
||||
const merchantPan = captureAfterLabel(body, "Merchant PAN");
|
||||
const terminalId = captureAfterLabel(body, "ID Terminal");
|
||||
const catatanRaw = captureAfterLabel(body, "Catatan");
|
||||
const biayaAdmin = parseIdrAmount(captureAfterLabel(body, "Biaya Admin"));
|
||||
|
||||
const rekening = captureAfterLabel(body, "Rekening Sumber Dana");
|
||||
const namaSumber = captureAfterLabel(body, "Nama Sumber Dana");
|
||||
const fundingParts = [namaSumber, rekening].filter(Boolean);
|
||||
const fundingSource =
|
||||
fundingParts.length > 0
|
||||
? fundingParts.join(" ").replace(/\s+/g, " ").trim()
|
||||
: null;
|
||||
|
||||
const details = {};
|
||||
if (payerName) details.payerName = payerName;
|
||||
if (merchant) details.merchant = merchant.trim();
|
||||
if (location) details.location = location.trim();
|
||||
if (jenisTransaksi) details.jenisTransaksi = jenisTransaksi.trim();
|
||||
if (acquirer) details.acquirer = acquirer.trim();
|
||||
if (issuer) details.issuer = issuer.trim();
|
||||
if (customerPan) details.customerPan = customerPan.trim();
|
||||
if (merchantPan) details.merchantPan = merchantPan.trim();
|
||||
if (terminalId) details.terminalId = terminalId.trim();
|
||||
if (catatanRaw != null && catatanRaw.trim() && catatanRaw.trim() !== "-") {
|
||||
details.catatan = catatanRaw.trim();
|
||||
}
|
||||
if (biayaAdmin != null) details.biayaAdmin = biayaAdmin;
|
||||
if (fundingSource) details.fundingSource = fundingSource;
|
||||
|
||||
return {
|
||||
amount,
|
||||
currency: "IDR",
|
||||
reference: reference.trim(),
|
||||
occurredAt,
|
||||
details,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export function toSpendingRecord(parsed, rule) {
|
||||
if (source === "livin") {
|
||||
description = parsed.details?.merchant || "";
|
||||
} else if (source === "bri") {
|
||||
description = parsed.details?.namaTujuan || "";
|
||||
description = parsed.details?.namaTujuan || parsed.details?.merchant || "";
|
||||
} else if (source === "grab") {
|
||||
if (parsed.kind === "grab.ereceipt.food" || parsed.details?.variant === "food") {
|
||||
const merchant = parsed.details?.merchant;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export const briQris = {
|
||||
kind: "bri.qris.success",
|
||||
source: "bri",
|
||||
fromAddress: "bankbri@bri.co.id",
|
||||
subject: "Pembelian QRIS Berhasil",
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { briQris } from "./bri-qris.js";
|
||||
import { briTransfer } from "./bri-transfer.js";
|
||||
import { grabEreceipt } from "./grab-ereceipt.js";
|
||||
import { livinQrPayment } from "./livin-qr-payment.js";
|
||||
|
||||
export const rules = [livinQrPayment, grabEreceipt, briTransfer];
|
||||
export const rules = [livinQrPayment, grabEreceipt, briTransfer, briQris];
|
||||
|
||||
Reference in New Issue
Block a user