import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { parseBriQris, parseBriTransfer } from "../src/parsers/bri.js"; import { parseFlipTransfer } from "../src/parsers/flip.js"; import { parseGrabReceipt, extractGrabTripTimesFromHtml } from "../src/parsers/grab.js"; import { parseLivinReceipt, splitMerchantLocation } from "../src/parsers/livin.js"; import { parseTokopediaOrder } from "../src/parsers/tokopedia.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 { flipTransfer } from "../src/rules/flip-transfer.js"; import { livinQrPayment } from "../src/rules/livin-qr-payment.js"; import { grabEreceipt } from "../src/rules/grab-ereceipt.js"; import { tokopediaOrder } from "../src/rules/tokopedia-order.js"; import { briQrisDirectBody, briQrisForwardedBody, briTransferDirectBody, briTransferForwardedBody, flipDirectBody, flipForwardedBody, grabBody, grabFoodForwardedBody, grabForwardedBody, grabRideHtml, grabTipForwardedBody, livinDirectBody, livinForwardedBody, tokopediaMultiForwardedBody, tokopediaSingleForwardedBody, } from "./fixtures.js"; describe("splitMerchantLocation", () => { it("splits clean multi-line penerima", () => { const r = splitMerchantLocation([ "Eleven Chicken Cabang Gah", "JAKARTA SELATAN - ID", ]); assert.equal(r.merchant, "Eleven Chicken Cabang Gah"); assert.equal(r.location, "JAKARTA SELATAN - ID"); }); it("keeps glued merchant+city as merchant only", () => { const r = splitMerchantLocation(["SNACK BAROKAHJAKARTA BARAT - ID"]); assert.equal(r.merchant, "SNACK BAROKAHJAKARTA BARAT - ID"); assert.equal(r.location, null); }); }); describe("parseLivinReceipt", () => { it("parses direct HTML-stripped body", () => { const r = parseLivinReceipt(livinDirectBody); assert.ok(r); assert.equal(r.amount, 14000); assert.equal(r.reference, "2609131121540314991"); assert.equal(r.occurredAt, "2026-09-13T14:48:31+07:00"); assert.equal(r.details.payerName, "NASYAROBBY NUGRAHA PUTRA"); assert.equal(r.details.merchant, "SNACK BAROKAHJAKARTA BARAT - ID"); assert.equal(r.details.location, undefined); assert.equal(r.details.qrisRef, "609532729372"); assert.equal(r.details.acquirer, "GoPay"); }); it("parses forwarded clean body", () => { const r = parseLivinReceipt(livinForwardedBody); assert.ok(r); assert.equal(r.amount, 22000); assert.equal(r.reference, "2609131121539559589"); assert.equal(r.occurredAt, "2026-09-13T10:43:51+07:00"); assert.equal(r.details.merchant, "Eleven Chicken Cabang Gah"); assert.equal(r.details.location, "JAKARTA SELATAN - ID"); }); it("returns null without amount/reference", () => { assert.equal(parseLivinReceipt("Halo someone\nPenerima\nFoo"), null); }); it("parses Indonesian month abbreviation (Agu)", () => { const body = `Halo NASYAROBBY NUGRAHA PUTRA, Penerima RM Bu Sum Ebony, KalibataJAKARTA SELATAN - ID Tanggal15 Agu 2026 Jam09:45:25 WIB Nominal TransaksiRp 15.000,00 No. Referensi2608151121545704363 No. Ref. QRIS608550990105`; const r = parseLivinReceipt(body); assert.ok(r); assert.equal(r.amount, 15000); assert.equal(r.reference, "2608151121545704363"); assert.equal(r.occurredAt, "2026-08-15T09:45:25+07:00"); }); }); describe("parseGrabReceipt", () => { it("parses direct Grab body", () => { const r = parseGrabReceipt(grabBody); assert.ok(r); assert.equal(r.kind, "grab.ereceipt.ride"); assert.equal(r.amount, 16600); assert.equal(r.reference, "A-9RXV4K2GWCLOAV"); assert.equal(r.occurredAt, "2026-09-13"); assert.equal(r.details.product, "Bike Hemat"); assert.equal(r.details.driver, "Mahendra"); assert.equal(r.details.passenger, "Bintang"); assert.deepEqual(r.details.compliments, ["Layanan Mantap"]); }); it("merges pickup time from HTML into occurredAt", () => { const r = parseGrabReceipt(grabBody, { html: grabRideHtml }); assert.ok(r); assert.equal(r.occurredAt, "2026-09-13T10:47:00+07:00"); assert.equal(r.details.dropoffAt, "2026-09-13T10:58:00+07:00"); }); it("extracts standalone AM/PM trip times from HTML", () => { const t = extractGrabTripTimesFromHtml(grabRideHtml); assert.equal(t.pickup, "10:47:00"); assert.equal(t.dropoff, "10:58:00"); }); it("parses forwarded Grab body", () => { const r = parseGrabReceipt(grabForwardedBody); assert.ok(r); assert.equal(r.amount, 16600); assert.equal(r.reference, "A-9RXV4K2GWCLOAV"); assert.equal(r.details.product, "Bike Hemat"); assert.equal(r.kind, "grab.ereceipt.ride"); }); it("parses Grab tip as a separate kind/invoice", () => { const r = parseGrabReceipt(grabTipForwardedBody); assert.ok(r); assert.equal(r.kind, "grab.ereceipt.tip"); assert.equal(r.variant, "tip"); assert.equal(r.amount, 2000); assert.equal(r.reference, "A-9RXV4K2GWCLOAV:tip"); assert.equal(r.occurredAt, "2026-09-13T10:47:00+07:00"); assert.equal(r.details.product, "Bike Hemat"); assert.equal(r.details.variant, "tip"); }); it("parses Grab Food receipt", () => { const r = parseGrabReceipt(grabFoodForwardedBody); assert.ok(r); assert.equal(r.kind, "grab.ereceipt.food"); assert.equal(r.variant, "food"); assert.equal(r.amount, 49000); assert.equal(r.reference, "00112712674-C8EUEZDGEEAZTX"); assert.equal(r.occurredAt, "2026-09-11T18:24:00+07:00"); assert.equal(r.details.merchant, "Doner Kebab - Kalibata City"); assert.equal(r.details.vehicleType, "GrabFood"); assert.equal(r.details.passenger, "Bintang"); assert.equal(r.details.paymentMethod, "OVO Cash"); }); }); describe("parseBriTransfer", () => { it("parses direct BRImo transfer body", () => { const r = parseBriTransfer(briTransferDirectBody); assert.ok(r); assert.equal(r.amount, 4000000); assert.equal(r.reference, "212189333864"); assert.equal(r.occurredAt, "2026-09-13T19:00:00+07:00"); assert.equal(r.details.payerName, "NASYAROBBY NUGRAHA PUTRA"); assert.equal(r.details.namaTujuan, "NELLY ARISANTI"); assert.equal(r.details.jenisTransaksi, "Transfer Bank BRI"); assert.equal(r.details.bankTujuan, "BANK BRI"); assert.equal(r.details.nomorTujuan, "1060 0100 2070 507"); assert.equal(r.details.biayaAdmin, 0); assert.equal( r.details.fundingSource, "NASYAROBBY NUGRAHA PUTRA 0121 **** **** 500", ); assert.equal(r.details.catatan, undefined); }); it("parses forwarded BRImo transfer body", () => { const r = parseBriTransfer(briTransferForwardedBody); assert.ok(r); assert.equal(r.amount, 4000000); assert.equal(r.reference, "212189333864"); assert.equal(r.details.namaTujuan, "NELLY ARISANTI"); }); it("returns null without amount/reference", () => { assert.equal(parseBriTransfer("Halo someone\nTransaksi Berhasil"), null); }); }); 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 = { kind: "livin.qr_payment.success", messageId: "1a099bd61efd234d", amount: 14000, reference: "2609131121540314991", occurredAt: "2026-09-13T14:48:31+07:00", details: { merchant: "SNACK BAROKAHJAKARTA BARAT - ID", qrisRef: "609532729372", }, }; const rec = toSpendingRecord(parsed, livinQrPayment); assert.equal(rec.invoice_id, "2609131121540314991"); assert.equal(rec.amount, 14000); assert.equal(rec.source, "livin"); assert.equal(rec.description, "SNACK BAROKAHJAKARTA BARAT - ID"); assert.equal(rec.category, ""); assert.equal(rec.message_id, "1a099bd61efd234d"); assert.equal(rec.trx_date, toPocketBaseDate(parsed.occurredAt)); assert.deepEqual(rec.parsed, parsed); }); it("maps Grab envelope to spendings fields", () => { const parsed = { kind: "grab.ereceipt.ride", messageId: "1a099b8b9d239e5e", amount: 16600, reference: "A-9RXV4K2GWCLOAV", occurredAt: "2026-09-13", details: { product: "Bike Hemat", driver: "Mahendra" }, }; const rec = toSpendingRecord(parsed, grabEreceipt); assert.equal(rec.invoice_id, "A-9RXV4K2GWCLOAV"); assert.equal(rec.source, "grab"); assert.equal(rec.description, "Grab Bike Hemat"); assert.equal(rec.trx_date, "2026-09-13 00:00:00.000Z"); assert.deepEqual(rec.parsed, parsed); }); it("maps Grab tip envelope to spendings fields", () => { const parsed = { kind: "grab.ereceipt.tip", messageId: "1a099e731cb511dc", amount: 2000, reference: "A-9RXV4K2GWCLOAV:tip", occurredAt: "2026-09-13T10:47:00+07:00", details: { product: "Bike Hemat", variant: "tip" }, }; const rec = toSpendingRecord(parsed, grabEreceipt); assert.equal(rec.invoice_id, "A-9RXV4K2GWCLOAV:tip"); assert.equal(rec.source, "grab"); assert.equal(rec.description, "Grab Bike Hemat tip"); assert.equal(rec.trx_date, toPocketBaseDate(parsed.occurredAt)); }); it("maps Grab Food envelope to spendings fields", () => { const parsed = { kind: "grab.ereceipt.food", messageId: "1a09a07cbd6f58cb", amount: 49000, reference: "00112712674-C8EUEZDGEEAZTX", occurredAt: "2026-09-11T18:24:00+07:00", details: { variant: "food", product: "Food", merchant: "Doner Kebab - Kalibata City", }, }; const rec = toSpendingRecord(parsed, grabEreceipt); assert.equal(rec.invoice_id, "00112712674-C8EUEZDGEEAZTX"); assert.equal(rec.source, "grab"); assert.equal(rec.description, "Grab Food Doner Kebab - Kalibata City"); }); it("maps BRI transfer envelope to spendings fields", () => { const parsed = { kind: "bri.transfer.success", messageId: "1a09b6cd9a3bc6bd", amount: 4000000, reference: "212189333864", occurredAt: "2026-09-13T19:00:00+07:00", details: { namaTujuan: "NELLY ARISANTI", jenisTransaksi: "Transfer Bank BRI", }, }; const rec = toSpendingRecord(parsed, briTransfer); assert.equal(rec.invoice_id, "212189333864"); assert.equal(rec.amount, 4000000); assert.equal(rec.source, "bri"); assert.equal(rec.description, "NELLY ARISANTI"); 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)); }); it("maps Tokopedia envelope to spendings fields", () => { const parsed = { kind: "tokopedia.order.completed", messageId: "1a09ba1248c7d2e8", amount: 48805, reference: "585871780568401286", occurredAt: "2026-09-05", details: { shop: "LuxePad.Creations" }, }; const rec = toSpendingRecord(parsed, tokopediaOrder); assert.equal(rec.invoice_id, "585871780568401286"); assert.equal(rec.source, "tokopedia"); assert.equal(rec.description, "LuxePad.Creations - Tokopedia"); assert.equal(rec.trx_date, "2026-09-05 00:00:00.000Z"); }); it("maps Flip transfer envelope to spendings fields", () => { const parsed = { kind: "flip.transfer.success", messageId: "1a0c704164e0cd54", amount: 40001, reference: "ST260918145355861M3ZG4", occurredAt: "2026-09-18T14:53:00+07:00", details: { namaTujuan: "Rwen Ibrahim", bankTujuan: "BCA", nomorRekening: "0020215810", }, }; const rec = toSpendingRecord(parsed, flipTransfer); assert.equal(rec.invoice_id, "ST260918145355861M3ZG4"); assert.equal(rec.amount, 40001); assert.equal(rec.source, "flip"); assert.equal(rec.description, "Rwen Ibrahim"); assert.equal(rec.trx_date, toPocketBaseDate(parsed.occurredAt)); assert.deepEqual(rec.parsed, parsed); }); }); describe("parseFlipTransfer", () => { it("parses direct stacked-label body", () => { const r = parseFlipTransfer(flipDirectBody); assert.ok(r); assert.equal(r.amount, 40001); assert.equal(r.reference, "ST260918145355861M3ZG4"); assert.equal(r.occurredAt, "2026-09-18T14:53:00+07:00"); assert.equal(r.details.namaTujuan, "Rwen Ibrahim"); assert.equal(r.details.bankTujuan, "BCA"); assert.equal(r.details.nomorRekening, "0020215810"); }); it("parses forwarded body", () => { const r = parseFlipTransfer(flipForwardedBody); assert.ok(r); assert.equal(r.amount, 40001); assert.equal(r.reference, "ST260918145355861M3ZG4"); assert.equal(r.occurredAt, "2026-09-18T14:53:00+07:00"); assert.equal(r.details.namaTujuan, "Rwen Ibrahim"); }); it("returns null when Nominal or ID Transaksi missing", () => { assert.equal(parseFlipTransfer("BUKTI TRANSFER\nNominal\nRp10.000\n"), null); assert.equal( parseFlipTransfer("ID Transaksi\n#ABC\nNama Tujuan\nX\n"), null, ); }); }); describe("parseTokopediaOrder", () => { it("parses single-item Pesanan Selesai", () => { const r = parseTokopediaOrder(tokopediaSingleForwardedBody); assert.ok(r); assert.equal(r.amount, 48805); assert.equal(r.reference, "585871780568401286"); assert.equal(r.occurredAt, "2026-09-05"); assert.equal(r.details.shop, "LuxePad.Creations"); assert.equal(r.details.trackingNumber, "JY1590448249"); assert.equal(r.details.shippingService, "Standard"); assert.equal(r.details.items.length, 1); assert.match(r.details.items[0].name, /Mouse Pad Besar Anime Style/); assert.equal(r.details.items[0].qty, 1); assert.equal(r.details.items[0].unitPrice, 88750); assert.equal(r.details.items[0].lineTotal, 88750); }); it("parses multi-item order as one invoice", () => { const r = parseTokopediaOrder(tokopediaMultiForwardedBody); assert.ok(r); assert.equal(r.amount, 26000); assert.equal(r.reference, "585494052374283654"); assert.equal(r.occurredAt, "2026-08-14"); assert.equal(r.details.shop, "NNcolection09"); assert.equal(r.details.items.length, 2); assert.match(r.details.items[0].name, /KOMBINASI BIRU/); assert.match(r.details.items[1].name, /KOMBINASI MERAH/); assert.equal(r.details.items[0].unitPrice, 10500); assert.equal(r.details.items[1].lineTotal, 10500); }); it("returns null without invoice/total", () => { assert.equal(parseTokopediaOrder("Hai Roby\nPesananmu selesai."), null); }); });