Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,17 @@ export type {
HorizonProberConfig,
} from "./horizonProber.js";

// Invoice calculator
export {
calculateSplitAmounts,
computeAmounts,
formatSplitPercentage,
} from "./invoice/calculator.js";

// Invoice memo search
export { searchByMemo } from "./search.js";
export type { SearchByMemoOptions } from "./search.js";

// AMM Calculator
export { estimateSwapOutput, calculatePoolShare } from "./ammCalculator.js";

Expand Down
35 changes: 34 additions & 1 deletion src/search.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Horizon } from "@stellar/stellar-sdk";
import type { InvoiceStatus } from "./types.js";
import type { Invoice, InvoiceStatus } from "./types.js";
import { SearchFailedError } from "./errors.js";

/** Query parameters for searching invoices. */
Expand All @@ -20,6 +20,39 @@ export interface SearchResult {
status: InvoiceStatus;
}

/** Options for {@link searchByMemo}. */
export interface SearchByMemoOptions {
/** When true, match exact case; otherwise ignore case (default). */
caseSensitive?: boolean;
}

/**
* Search a local array of invoices by memo content.
*
* @param invoices - Array of invoices to search.
* @param query - Substring to look for in each invoice's memo.
* @param opts - Search options.
* @returns Invoices whose memo contains `query`; all invoices if `query` is empty.
*/
export function searchByMemo(
invoices: Invoice[],
query: string,
opts?: SearchByMemoOptions,
): Invoice[] {
if (query === "") {
return invoices;
}

const needle = opts?.caseSensitive ? query : query.toLowerCase();
return invoices.filter((invoice) => {
if (invoice.memo === undefined || invoice.memo === null) {
return false;
}
const haystack = opts?.caseSensitive ? invoice.memo : invoice.memo.toLowerCase();
return haystack.includes(needle);
});
}

/**
* Search invoices by partial criteria using Horizon API.
*
Expand Down
78 changes: 78 additions & 0 deletions test/searchByMemo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, expect, it } from "vitest";
import { searchByMemo } from "../src/search.js";
import type { Invoice } from "../src/types.js";

function makeInvoice(id: string, memo?: string | null): Invoice {
return {
id,
creator: "GABC",
recipients: [],
token: "TOKEN",
deadline: 0,
funded: 0n,
status: "Pending",
payments: [],
memo: memo as unknown as undefined,
};
}

describe("searchByMemo", () => {
const invoices: Invoice[] = [
makeInvoice("inv-1", "split:INV-001"),
makeInvoice("inv-2", "SPLIT:inv-002"),
makeInvoice("inv-3", "monthly payment"),
makeInvoice("inv-4", "split:"),
makeInvoice("inv-5", ""),
makeInvoice("inv-6", null as unknown as undefined),
makeInvoice("inv-7", undefined),
];

it("returns invoices whose memo contains the query substring", () => {
const result = searchByMemo(invoices, "split");
expect(result.map((i) => i.id)).toEqual(["inv-1", "inv-2", "inv-4"]);
});

it("is case-insensitive by default", () => {
const result = searchByMemo(invoices, "SPLIT");
expect(result.map((i) => i.id)).toEqual(["inv-1", "inv-2", "inv-4"]);
});

it("can be made case-sensitive", () => {
const result = searchByMemo(invoices, "SPLIT", { caseSensitive: true });
expect(result.map((i) => i.id)).toEqual(["inv-2"]);
});

it("returns all invoices when query is empty", () => {
const result = searchByMemo(invoices, "");
expect(result.map((i) => i.id)).toEqual([
"inv-1",
"inv-2",
"inv-3",
"inv-4",
"inv-5",
"inv-6",
"inv-7",
]);
});

it("skips invoices with null or undefined memo", () => {
const result = searchByMemo(invoices, "payment");
expect(result.map((i) => i.id)).toEqual(["inv-3"]);
});

it("returns empty array when no memo matches", () => {
const result = searchByMemo(invoices, "nonexistent");
expect(result).toEqual([]);
});

it("matches partial substrings anywhere in the memo", () => {
const result = searchByMemo(invoices, "nv-00");
expect(result.map((i) => i.id)).toEqual(["inv-1", "inv-2"]);
});

it("does not mutate the input array", () => {
const before = invoices.map((i) => i.id);
searchByMemo(invoices, "split");
expect(invoices.map((i) => i.id)).toEqual(before);
});
});