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:
2026-09-13 23:28:59 +07:00
parent 1b880ed554
commit 0fa0ed3380
9 changed files with 266 additions and 6 deletions
+74 -1
View File
@@ -1,13 +1,16 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { parseBriTransfer } from "../src/parsers/bri.js";
import { parseBriQris, parseBriTransfer } from "../src/parsers/bri.js";
import { parseGrabReceipt, extractGrabTripTimesFromHtml } from "../src/parsers/grab.js";
import { parseLivinReceipt, splitMerchantLocation } from "../src/parsers/livin.js";
import { toSpendingRecord, toPocketBaseDate } from "../src/pocketbase/map.js";
import { briQris } from "../src/rules/bri-qris.js";
import { briTransfer } from "../src/rules/bri-transfer.js";
import { livinQrPayment } from "../src/rules/livin-qr-payment.js";
import { grabEreceipt } from "../src/rules/grab-ereceipt.js";
import {
briQrisDirectBody,
briQrisForwardedBody,
briTransferDirectBody,
briTransferForwardedBody,
grabBody,
@@ -177,6 +180,56 @@ describe("parseBriTransfer", () => {
});
});
describe("parseBriQris", () => {
it("parses direct BRImo QRIS body", () => {
const r = parseBriQris(briQrisDirectBody);
assert.ok(r);
assert.equal(r.amount, 674500);
assert.equal(r.reference, "185794483464");
assert.equal(r.occurredAt, "2026-07-30T19:21:34+07:00");
assert.equal(r.details.payerName, "NASYAROBBY NUGRAHA PUTRA");
assert.equal(r.details.merchant, "Shopee Indonesia");
assert.equal(r.details.location, "KOTA JAKARTA");
assert.equal(r.details.jenisTransaksi, "QRIS Bayar");
assert.equal(r.details.acquirer, "Shopeepay");
assert.equal(r.details.issuer, "BRI");
assert.equal(r.details.customerPan, "9360000210106600585");
assert.equal(r.details.merchantPan, "936009180021573212");
assert.equal(r.details.terminalId, "21573212");
assert.equal(r.details.biayaAdmin, 0);
assert.equal(
r.details.fundingSource,
"NASYAROBBY NUGRAHA PUTRA 0121 **** **** 500",
);
});
it("parses forwarded BRImo QRIS body", () => {
const r = parseBriQris(briQrisForwardedBody);
assert.ok(r);
assert.equal(r.amount, 674500);
assert.equal(r.reference, "185794483464");
assert.equal(r.details.merchant, "Shopee Indonesia");
});
it("falls back to Total Transaksi when Nominal missing", () => {
const body = `Halo FOO,
Total Transaksi
12.500
Nomor Referensi 999
Tanggal Transaksi 1 Jan 2026, 10:00:00 WIB
Nama Merchant Cafe
`;
const r = parseBriQris(body);
assert.ok(r);
assert.equal(r.amount, 12500);
assert.equal(r.reference, "999");
});
it("returns null without amount/reference", () => {
assert.equal(parseBriQris("Halo someone\nNama Merchant Foo"), null);
});
});
describe("toSpendingRecord", () => {
it("maps Livin envelope to spendings fields", () => {
const parsed = {
@@ -273,4 +326,24 @@ describe("toSpendingRecord", () => {
assert.equal(rec.trx_date, toPocketBaseDate(parsed.occurredAt));
assert.deepEqual(rec.parsed, parsed);
});
it("maps BRI QRIS envelope to spendings fields", () => {
const parsed = {
kind: "bri.qris.success",
messageId: "1a09b9031c233407",
amount: 674500,
reference: "185794483464",
occurredAt: "2026-07-30T19:21:34+07:00",
details: {
merchant: "Shopee Indonesia",
location: "KOTA JAKARTA",
},
};
const rec = toSpendingRecord(parsed, briQris);
assert.equal(rec.invoice_id, "185794483464");
assert.equal(rec.amount, 674500);
assert.equal(rec.source, "bri");
assert.equal(rec.description, "Shopee Indonesia");
assert.equal(rec.trx_date, toPocketBaseDate(parsed.occurredAt));
});
});