Skip to content
Open
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
35 changes: 28 additions & 7 deletions __tests__/invoiceFilters.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { applyInvoiceFilters, countActiveInvoiceFilters, EMPTY_INVOICE_FILTERS } from "../hooks/useInvoiceFilters";
import type { Invoice } from "../utils/soroban";
import { applyInvoiceFilters, countActiveInvoiceFilters, EMPTY_INVOICE_FILTERS } from "../src/hooks/useInvoiceFilters";
import type { Invoice } from "../src/utils/soroban";

function makeInvoice(
id: bigint,
Expand All @@ -9,11 +9,12 @@ function makeInvoice(
dueDate: bigint,
discountRate: number,
token?: string,
payer = "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBRY",
): Invoice {
return {
id,
freelancer: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
payer: "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBRY",
payer,
amount,
due_date: dueDate,
discount_rate: discountRate,
Expand All @@ -26,9 +27,9 @@ function makeInvoice(

describe("invoice filter logic", () => {
const invoices: Invoice[] = [
makeInvoice(101n, "Pending", 100n * 10_000_000n, 1_760_000_000n, 300, "token-usdc"),
makeInvoice(202n, "Funded", 500n * 10_000_000n, 1_770_000_000n, 900, "token-eurc"),
makeInvoice(303n, "Paid", 900n * 10_000_000n, 1_780_000_000n, 1200, "token-xlm"),
makeInvoice(101n, "Pending", 100n * 10_000_000n, 1_760_000_000n, 300, "token-usdc", "PAYER_LOW"),
makeInvoice(202n, "Funded", 500n * 10_000_000n, 1_770_000_000n, 900, "token-eurc", "PAYER_HIGH"),
makeInvoice(303n, "Paid", 900n * 10_000_000n, 1_780_000_000n, 1200, "token-xlm", "PAYER_UNKNOWN"),
];

it("searches by id and address fragments", () => {
Expand Down Expand Up @@ -68,6 +69,25 @@ describe("invoice filter logic", () => {
expect(filtered.map((invoice) => invoice.id)).toEqual([202n]);
});

it("filters by minimum payer reputation score", () => {
const filtered = applyInvoiceFilters(
invoices,
{
...EMPTY_INVOICE_FILTERS,
minPayerReputation: "75",
},
{
resolvePayerReputation: (invoice) => {
if (invoice.payer === "PAYER_LOW") return 40;
if (invoice.payer === "PAYER_HIGH") return 90;
return null;
},
},
);

expect(filtered.map((invoice) => invoice.id)).toEqual([202n]);
});

it("counts active filter groups correctly", () => {
expect(
countActiveInvoiceFilters({
Expand All @@ -76,7 +96,8 @@ describe("invoice filter logic", () => {
statuses: ["Pending"],
minAmount: "10",
token: "USDC",
minPayerReputation: "80",
}),
).toBe(4);
).toBe(5);
});
});
2 changes: 2 additions & 0 deletions src/components/InvoiceFilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type InvoiceFilterBarProps = {
onClearFilters: () => void;
activeFilterCount: number;
className?: string;
showReputationFilter?: boolean;
};

const TOKEN_OPTIONS = ["USDC", "EURC", "XLM"] as const;
Expand All @@ -23,6 +24,7 @@ export default function InvoiceFilterBar({
onClearFilters,
activeFilterCount,
className,
showReputationFilter = false,
}: InvoiceFilterBarProps) {
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);

Expand Down
6 changes: 6 additions & 0 deletions src/components/LPDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ export default function LPDashboard() {
onFiltersChange={setFilters}
onClearFilters={clearFilters}
activeFilterCount={activeFilterCount}
showReputationFilter
/>
<ExportButton data={filteredInvoices} filenamePrefix="iln-lp-export" />
<button
Expand Down Expand Up @@ -717,6 +718,11 @@ export default function LPDashboard() {
risk={payerRisks.get(invoice.payer) ?? "Unknown"}
score={payerScores.get(invoice.payer) ?? null}
/>
{payerScores.get(invoice.payer) ? (
<span className="mt-1 block text-[10px] font-bold uppercase tracking-wide text-on-surface-variant">
Score {payerScores.get(invoice.payer)?.score}/100
</span>
) : null}
</td>
)}
<td className="px-6 py-5 text-right">
Expand Down
17 changes: 16 additions & 1 deletion src/hooks/useInvoiceFilters.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useCallback, useMemo } from "react";
import { useCallback, useEffect, useMemo } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import type { Invoice } from "@/utils/soroban";
import { tokenAmountToNumber } from "@/utils/format";
Expand Down Expand Up @@ -88,6 +88,11 @@ function buildFilterQuery(searchParams: URLSearchParams, namespace: string, filt

function readFiltersFromParams(searchParams: URLSearchParams, namespace: string): InvoiceFilters {
const prefix = `${namespace}_`;
const persistedReputation =
typeof window === "undefined"
? ""
: window.localStorage.getItem(`${prefix}minPayerReputation`) ?? "";

return {
search: searchParams.get(`${prefix}search`) ?? "",
statuses: cleanStatusList((searchParams.get(`${prefix}statuses`) ?? "").split(",").filter(Boolean)),
Expand Down Expand Up @@ -132,6 +137,7 @@ export function applyInvoiceFilters(
const start = filters.startDate ? new Date(`${filters.startDate}T00:00:00.000Z`) : null;
const end = filters.endDate ? new Date(`${filters.endDate}T23:59:59.999Z`) : null;
const selectedToken = filters.token.trim().toUpperCase();
const minPayerReputation = parseNumeric(filters.minPayerReputation);

return invoices.filter((invoice) => {
if (search) {
Expand Down Expand Up @@ -207,6 +213,15 @@ export function useInvoiceFilters({ namespace }: UseInvoiceFiltersOptions) {

const activeFilterCount = useMemo(() => countActiveInvoiceFilters(filters), [filters]);

useEffect(() => {
const key = `${namespace}_minPayerReputation`;
if (filters.minPayerReputation.trim()) {
window.localStorage.setItem(key, filters.minPayerReputation.trim());
} else {
window.localStorage.removeItem(key);
}
}, [filters.minPayerReputation, namespace]);

return {
filters,
setFilters: updateFilters,
Expand Down