import { describe, expect, test } from "bun:test"; import { parseForm4Xml } from "./insider-data"; import { buildInsiderRows, buildInsiderSummary, parseInsiderFiling } from "./model"; import type { SecFilingItem } from "../../../types/data-provider"; const owner = `OWNER & CO123CEO`; function transaction(code: string, security: string, shares: string, price: string, date = "2026-08-20", derivative = false) { const tag = derivative ? "derivativeTransaction" : "nonDerivativeTransaction"; return `<${tag}> ${security} ${date} ${code} ${shares}${price}D 292786 IFamily trust `; } const filing: SecFilingItem = { accessionNumber: "one", form: "4", filingDate: new Date("2026-08-24"), cik: "123", filingUrl: "https://www.sec.gov/one" }; describe("Form 4 transaction parsing", () => { test("keeps all gift, conversion and derivative rows with their own dates, security and ownership", () => { const xml = `${owner} Unrelated holding999999 ${transaction("G", "Class A Common Stock", "55498", "0", "2026-08-10")} ${transaction("G", "Class A Common Stock", "294502", "0", "2026-08-11")} ${transaction("C", "Class B Common Stock", "402348", "")} ${transaction("M", "Restricted Stock Units", "877500", "0", "2026-08-20", true)}`; const rows = parseForm4Xml(xml); expect(rows).toHaveLength(4); expect(rows.map((row) => [row.transactionType, row.shares, row.pricePerShare, row.filingDate?.toISOString().slice(0, 10), row.isDerivative])).toEqual([ ["G", 55498, 0, "2026-08-10", false], ["G", 294502, 0, "2026-08-11", false], ["C", 402348, null, "2026-08-20", false], ["M", 877500, 0, "2026-08-20", true], ]); expect(rows[0]).toMatchObject({ reportedName: "OWNER & CO", securityTitle: "Class A Common Stock", acquiredDisposed: "D", ownershipType: "I", ownershipNature: "Family trust", sharesOwned: 292786, totalValue: 0 }); expect(rows[2]?.totalValue).toBeNull(); const projected = buildInsiderRows(parseInsiderFiling(filing, xml)); expect(new Set(projected.map((row) => row.id)).size).toBe(4); expect(projected[1]).toMatchObject({ filingDate: "2026-08-24T00:00:00.000Z", transactionDate: "2026-08-11T00:00:00.000Z", side: "GIFT", transactionCode: "G", reportingOwners: [{ name: "OWNER & CO", cik: "123", title: "CEO" }] }); }); test("missing amounts and invalid dates remain unknown rather than becoming zero or today", () => { const rows = parseForm4Xml(`${owner}${transaction("F", "Class A", "", "", "2026-02-31")}`); expect(rows[0]).toMatchObject({ filingDate: null, shares: null, pricePerShare: null, totalValue: null, transactionType: "F" }); expect(parseForm4Xml("not a filing")).toEqual([]); expect(buildInsiderRows(parseInsiderFiling(filing, ""))[0]?.status).toBe("unavailable"); }); test("a director without an officer title is labeled, and a Form 4 without transaction lines is a disclosure, not a parse failure", () => { const director = `DIRECTOR A91`; expect(parseForm4Xml(`${director}${transaction("S", "Common Stock", "10", "5")}`)[0]?.title).toBe("Director"); const exit = parseInsiderFiling(filing, `${owner}No longer subject to Section 16.`); expect(buildInsiderRows(exit)[0]).toMatchObject({ status: "disclosure", remarks: "No longer subject to Section 16." }); expect(buildInsiderSummary(exit, new Date("2026-09-10").getTime())).toBe("Loaded filings, last 90 days: no parsed non-derivative buys/sales."); }); test("summary excludes exercises and gifts, separates securities, and exposes missing sale prices", () => { const xml = `${owner} ${transaction("S", "Class A", "100", "10")}${transaction("S", "Class A", "25", "")} ${transaction("S", "Class B", "50", "20")}${transaction("S", "Options", "9000", "1", "2026-08-20", true)} ${transaction("G", "Class A", "8000", "0")}${transaction("M", "Class A", "7000", "0")} ${transaction("S", "Class A", "6000", "1", "2025-01-01")} `; const summary = buildInsiderSummary(parseInsiderFiling(filing, xml), new Date("2026-09-10").getTime()); expect(summary).toBe("Loaded filings, last 90 days: Class A: Sold 125 shares (value unavailable) | Class B: Sold 50 shares ($1,000.00)"); }); });