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
5 changes: 3 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| | |
|---|---|
| **Last updated** | 2026-11-01 (fix/scorecard-hardening: M3-33 DONE) |
| **Last updated** | 2026-11-02 (feat/payee-csv-export-76: X-05 DONE) |
| **Docs baseline** | v0.3, worldwide scope (`PRD.md`, `ARCHITECTURE.md`, `ARCHITECTURE_ESSENTIALS.md`, `AGENTS.md`, `CLAUDE.md`, `project_structure.md`) |
| **Current phase** | Phase 0 (Foundations) → starting Phase 1 (M0 Validate) |
| **Readiness** | See [§3](#3-progress-snapshot) |
Expand Down Expand Up @@ -394,7 +394,7 @@ Resolve each with an ADR and link it here. Move resolved rows to the bottom with
| X-02 | `LCK-10` Payee "applied" acknowledgment (contract event) and UI | contracts | P1 | TODO | P-05 | Event emitted and shown on receipt; ADR if contract surface changes |
| X-03 | `REG-7` New-payee release delay in contract; set parameters via ADR | contracts | P1 | TODO | DEC-14 | Property tests updated; docs updated |
| X-04 | `SND-8` Email notifications (opt-in, provider-agnostic, `subscriptions` table) | sdk | P1 | TODO | P-05 | Opt-in and unsubscribe work; no personal data in logs |
| X-05 | `PAY-6` Payee CSV export | app | P1 | TODO | M3-11 | Export matches dashboard |
| X-05 | `PAY-6` Payee CSV export | app | P1 | DONE | M3-11 | Export matches dashboard |
| X-06 | `PLT-4` Per-lock and global caps: set values and test enforcement | contracts | P1 | TODO | M1-10 | Cap breach rejected in tests; values documented |
| X-07 | `PLT-5` Upgrade timelock before mainnet | contracts | P1 | TODO | M1-20 | Timelock enforced; upgrade runbook updated |
| X-08 | Re-run property, budget, and integration suites after P1 contract changes | contracts | P1 | TODO | X-02, X-03, X-06, X-07 | All green; budgets recorded |
Expand Down Expand Up @@ -542,6 +542,7 @@ Newest first. One entry per PR. Required for every contribution (see §2).

| Date | PR / ref | Repo | Rows touched | Summary |
|---|---|---|---|---|
| 2026-11-02 | `feat/payee-csv-export-76` | app | X-05 DONE | Closes #76: added ExportCSVButton and lib/csv.ts for payee lock history export with RFC-4180 escaping, BigInt base units conversion to USDC decimals, and ISO 8601 UTC date formatting; externalized strings in messages/en.json. All 214 tests pass and build clean. |
| 2026-11-01 | `fix/scorecard-hardening` | app | M3-33 DONE | OpenSSF Scorecard hardening: pinned malware-scan.yml actions to commit SHAs and semgrep to 1.136.0 (Pinned-Dependencies); removed curl-pipe downloadThenRun pattern in e2e.yml; enriched SECURITY.md with vulnerability reporting instructions and response/disclosure timelines. All 187 tests pass. |
| 2026-11-01 | `docs/drips-wave-application` | app | no row changes | Added dedicated DRIPS_WAVE.md application and review summary document for the Drips Wave Stellar program review panel, detailing Next.js web application architecture, live Vercel testnet deployment, strict privacy boundaries, testing hygiene, and maintainers. |
| 2026-11-01 | `feat/route-loading-error-states` | app | M3-27 DONE | Closes #37: added route-level designed states (app/loading.tsx, app/locks/loading.tsx, app/payee/loading.tsx, app/error.tsx with autofocus retry, and honest app/not-found.tsx); added accessible Spinner and StatePanel in components/ui/; externalized strings to messages/en.json. |
Expand Down
6 changes: 6 additions & 0 deletions app/payee/PayeeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useId } from "react";
import { AmountDisplay } from "@/components/money/AmountDisplay";
import { ExportCSVButton } from "@/components/payee/ExportCSVButton";
import { DateTimeDisplay } from "@/components/time/DateTimeDisplay";
import { ASSET_CODE } from "@/lib/constants";
import { shortAddress } from "@/lib/i18n/format";
Expand Down Expand Up @@ -94,6 +95,11 @@ export function PayeeView({ payees }: { payees: IndexedPayee[] }) {

{d.phase === "loading" ? <p role="status">{t("pages.payee.loading")}</p> : null}
{d.phase === "readFailed" ? <p role="alert">{t("pages.payee.readFailed")}</p> : null}
{d.phase === "ready" ? (
<div className="flex items-center justify-end">
<ExportCSVButton locks={d.locks} disabled={d.locks.length === 0} />
</div>
) : null}
{d.phase === "ready" && d.locks.length === 0 ? <p>{t("pages.payee.empty")}</p> : null}
{d.phase === "ready" && d.locks.length > 0 ? (
<ul className="flex flex-col gap-3">
Expand Down
85 changes: 85 additions & 0 deletions components/payee/ExportCSVButton.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it, vi } from "vitest";
import * as csvModule from "@/lib/csv";
import { t } from "@/lib/i18n/messages";
import { ExportCSVButton } from "./ExportCSVButton";

describe("ExportCSVButton component", () => {
it("renders a button element with externalized export label", () => {
const element = ExportCSVButton({
locks: [
{
id: "1",
sender: "GBBM6WDFDWINJB4GO6GCT6EVK375WZWW4NPWBFFEBAMZTXFOWA3KITSK",
total: "10000000",
released: "0",
state: "Open",
expiresAt: "2026-12-31T00:00:00.000Z",
createdAt: "2026-10-01T00:00:00.000Z",
},
],
});

expect(element.type).toBe("button");
expect(element.props.type).toBe("button");
expect(element.props.children).toBe(t("pages.payee.exportCsv"));
expect(element.props["aria-label"]).toBe(t("pages.payee.exportCsv"));
expect(element.props.disabled).toBe(false);
});

it("is disabled when locks array is empty", () => {
const element = ExportCSVButton({
locks: [],
});

expect(element.props.disabled).toBe(true);
});

it("is disabled when disabled prop is explicitly true even with locks", () => {
const element = ExportCSVButton({
locks: [
{
id: "1",
sender: "GABC",
total: "10000000",
released: "0",
state: "Open",
expiresAt: "2026-12-31T00:00:00.000Z",
createdAt: "2026-10-01T00:00:00.000Z",
},
],
disabled: true,
});

expect(element.props.disabled).toBe(true);
});

it("triggers CSV formatting and download on click", () => {
const formatLocksSpy = vi.spyOn(csvModule, "formatLocksToCSV");
const downloadCSVSpy = vi.spyOn(csvModule, "downloadCSV").mockImplementation(() => {});

const mockLocks = [
{
id: "5",
sender: "GSENDER",
total: 50_000_000n,
released: 10_000_000n,
unlockDate: "2026-11-15T00:00:00.000Z",
status: "Open",
createdAt: "2026-10-15T00:00:00.000Z",
},
];

const element = ExportCSVButton({
locks: mockLocks,
filename: "custom-export.csv",
});

element.props.onClick();

expect(formatLocksSpy).toHaveBeenCalledWith(mockLocks);
expect(downloadCSVSpy).toHaveBeenCalledWith(expect.any(String), "custom-export.csv");

formatLocksSpy.mockRestore();
downloadCSVSpy.mockRestore();
});
});
39 changes: 39 additions & 0 deletions components/payee/ExportCSVButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { downloadCSV, formatLocksToCSV, type LockSummary } from "@/lib/csv";
import { t } from "@/lib/i18n/messages";

export interface ExportCSVButtonProps {
locks: LockSummary[];
disabled?: boolean;
filename?: string;
className?: string;
}

/**
* Client-side CSV export button for payee lock history (Roadmap X-05 / PAY-6).
* Enables institutional payees (schools, landlords) to export locks for reconciliation.
*/
export function ExportCSVButton({
locks,
disabled = false,
filename,
className = "self-start rounded border px-3 py-1.5 text-sm font-medium hover:bg-neutral-50 dark:hover:bg-neutral-900 disabled:opacity-50",
}: ExportCSVButtonProps) {
function handleExport() {
const csv = formatLocksToCSV(locks);
downloadCSV(csv, filename);
}

return (
<button
type="button"
onClick={handleExport}
disabled={disabled || locks.length === 0}
className={className}
aria-label={t("pages.payee.exportCsv")}
>
{t("pages.payee.exportCsv")}
</button>
);
}
Loading
Loading