feat: add gmail-hook monorepo with watcher, api, and web

Ship the receipt pipeline (Livin/Grab/BRI), PocketBase spendings, PM2 ecosystem, and a proper .gitignore that excludes secrets, SQLite data, and media dumps.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-13 22:58:47 +07:00
co-authored by Cursor
parent 8fa9f4350e
commit 46a2e2a7b0
67 changed files with 12153 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { 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 { 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 {
briTransferDirectBody,
briTransferForwardedBody,
grabBody,
grabFoodForwardedBody,
grabForwardedBody,
grabRideHtml,
grabTipForwardedBody,
livinDirectBody,
livinForwardedBody,
} 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("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);
});
});