feat(watcher): add Flip transfer receipt provider

Match no-reply@flip.id transfer success mail (direct or forwarded), parse stacked Nominal/ID Transaksi fields, and map spendings with source=flip.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 10:09:25 +07:00
co-authored by Cursor
parent f270f531ea
commit 427636b59b
9 changed files with 376 additions and 0 deletions
+56
View File
@@ -1,12 +1,14 @@
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";
@@ -15,6 +17,8 @@ import {
briQrisForwardedBody,
briTransferDirectBody,
briTransferForwardedBody,
flipDirectBody,
flipForwardedBody,
grabBody,
grabFoodForwardedBody,
grabForwardedBody,
@@ -366,6 +370,58 @@ describe("toSpendingRecord", () => {
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", () => {