Implement parsing for Tokopedia "Pesanan Selesai" messages, including support for single and multi-item orders. Update rules to recognize Tokopedia order completion and enhance spending record mapping. Add tests to validate new functionality.
127 lines
3.4 KiB
JavaScript
127 lines
3.4 KiB
JavaScript
import {
|
|
captureAfterLabel,
|
|
parseDayMonthYear,
|
|
parseIdrAmount,
|
|
} from "../lib/parse-helpers.js";
|
|
|
|
const QTY_LINE = /^(\d+)\s*x\s*(Rp[\d.,]+)\s*$/i;
|
|
const RP_ONLY = /^(Rp[\d.,]+)\s*$/i;
|
|
const URL_LINE = /^(?:https?:\/\/|<https?:\/\/)/i;
|
|
|
|
function isUrlLine(line) {
|
|
return URL_LINE.test(line.trim());
|
|
}
|
|
|
|
/**
|
|
* Extract product line items from the Toko…Subtotal block.
|
|
* Handles single- and multi-item orders from the same shop.
|
|
*/
|
|
export function extractTokopediaItems(itemBlock) {
|
|
if (!itemBlock) return [];
|
|
|
|
const lines = itemBlock
|
|
.split(/\r?\n/)
|
|
.map((l) => l.trim())
|
|
.filter((l) => l && !isUrlLine(l));
|
|
|
|
const items = [];
|
|
let nameParts = [];
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const qtyMatch = line.match(QTY_LINE);
|
|
if (qtyMatch) {
|
|
const qty = Number(qtyMatch[1]);
|
|
const unitPrice = parseIdrAmount(qtyMatch[2]);
|
|
let lineTotal = unitPrice;
|
|
const next = lines[i + 1];
|
|
if (next && RP_ONLY.test(next)) {
|
|
lineTotal = parseIdrAmount(next);
|
|
i += 1;
|
|
}
|
|
const name = nameParts.join(" ").replace(/\s+/g, " ").trim();
|
|
nameParts = [];
|
|
if (name && unitPrice != null) {
|
|
items.push({
|
|
name,
|
|
qty,
|
|
unitPrice,
|
|
lineTotal: lineTotal ?? unitPrice,
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
nameParts.push(line);
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
function extractItemBlock(body) {
|
|
const tokoIdx = body.search(/Toko\s*:/i);
|
|
if (tokoIdx < 0) return null;
|
|
|
|
const afterToko = body.slice(tokoIdx);
|
|
const shopLineEnd = afterToko.search(/\r?\n/);
|
|
const rest =
|
|
shopLineEnd >= 0 ? afterToko.slice(shopLineEnd + 1) : afterToko;
|
|
|
|
const endMatch = rest.search(/Subtotal harga produk|Total belanja/i);
|
|
return endMatch >= 0 ? rest.slice(0, endMatch) : rest;
|
|
}
|
|
|
|
function parseTotalBelanja(body) {
|
|
const m = body.match(/Total\s+belanja\s*\r?\n\s*(Rp[\d.,]+)/i);
|
|
if (m) return parseIdrAmount(m[1]);
|
|
// Fallback: value on same line
|
|
return parseIdrAmount(captureAfterLabel(body, "Total belanja"));
|
|
}
|
|
|
|
/**
|
|
* Parse Tokopedia “Pesanan Selesai” body (plain text).
|
|
* Required: No.Invoice + Total belanja. One row per invoice.
|
|
*/
|
|
export function parseTokopediaOrder(body) {
|
|
if (!body) return null;
|
|
|
|
const invoiceMatch = body.match(/No\.?\s*Invoice\s*:?\s*(\d+)/i);
|
|
const reference = invoiceMatch?.[1] || null;
|
|
|
|
const amount = parseTotalBelanja(body);
|
|
if (amount == null || !reference) return null;
|
|
|
|
const cleanRef = reference.trim();
|
|
if (!cleanRef) return null;
|
|
|
|
const shopMatch = body.match(/Toko\s*:\s*([^\n<]+)/i);
|
|
const shop = shopMatch
|
|
? shopMatch[1].replace(/[<\s]*https?:\/\/.*$/i, "").trim() || null
|
|
: null;
|
|
|
|
const items = extractTokopediaItems(extractItemBlock(body));
|
|
|
|
const tanggalRaw = captureAfterLabel(body, "Tanggal Terima");
|
|
const occurredAt = parseDayMonthYear(tanggalRaw);
|
|
|
|
const trackingNumber = captureAfterLabel(body, "Nomor Resi");
|
|
const shippingService = captureAfterLabel(body, "Layanan Pengiriman");
|
|
|
|
const details = {};
|
|
if (shop) details.shop = shop;
|
|
if (items.length) details.items = items;
|
|
if (trackingNumber && !isUrlLine(trackingNumber)) {
|
|
details.trackingNumber = trackingNumber.trim();
|
|
}
|
|
if (shippingService && !isUrlLine(shippingService)) {
|
|
details.shippingService = shippingService.trim();
|
|
}
|
|
|
|
return {
|
|
amount,
|
|
currency: "IDR",
|
|
reference: cleanRef,
|
|
occurredAt,
|
|
details,
|
|
};
|
|
}
|