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
119 changes: 119 additions & 0 deletions components/proof/ProofLedger.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* @vitest-environment jsdom */

import { afterEach, describe, expect, it, vi } from "vitest";
import "@testing-library/jest-dom/vitest";
import { render, screen, waitFor } from "@testing-library/react";
import React from "react";

import { ProofLedger } from "./ProofLedger";
import type { BuyerProofLedger } from "@/lib/proof-ledger/buyer-proof-ledger";

/**
* ProofLedger honesty contract (readiness copy at proof_events=0).
*
* At proof_events=0 the buyer has recovered no real dollar yet, so the header
* MUST read as future/readiness ("will land here") + carry a DOM-measurable
* Sample marker. The present-tense "we win back ... lands here" claim is only
* honest once real proof_events exist. This guards against the recovered-as-fact
* overclaim (FTC 5 / substantiation) the honesty gate forbids while proof=0.
*/

function makeLedger(proofEvents: number): BuyerProofLedger {
return {
tenant_id: "tenant-1",
owner_approval_policy: {
owner_approval_required: true,
outbound_requires_owner_approval: true,
no_automatic_customer_contact: true,
},
recovery_numbers_basis: "projected",
billing_live: false,
totals: {
proof_events: proofEvents,
outcome_receipts: 0,
pending_receipts: 0,
wins: 0,
verified_wins: 0,
projected_recovery_cents: 0,
verified_projected_recovery_cents: 0,
blocked_events: 0,
},
sources: {
supabase: { status: "ok" },
pulse_blocked_events: { status: "not_configured" },
},
proof_events: Array.from({ length: proofEvents }, (_, i) => ({
id: `event-${i}`,
event_type: "approved",
recovery_item_id: null,
opportunity_id: null,
projected_value_cents: 0,
recovery_numbers_basis: "projected" as const,
owner_approved: true,
outcome_basis: "owner_attested" as const,
created_at: "2026-06-10T00:00:00.000Z",
})),
outcome_receipts: [],
blocked_events: [],
};
}

function stubFetchWith(ledger: BuyerProofLedger) {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({ ok: true, json: async () => ledger }),
);
}

afterEach(() => {
vi.unstubAllGlobals();
});

describe("ProofLedger - SAMPLE state (zero proof_events)", () => {
it("renders readiness (future-tense) copy, not the present-tense recovered claim", async () => {
stubFetchWith(makeLedger(0));
const { container } = render(<ProofLedger />);

await waitFor(() =>
expect(container.querySelector('[data-proof-state]')).not.toBeNull(),
);

// Future/readiness tense is honest at proof=0: money "will land here".
expect(screen.getByText(/will land here/i)).toBeInTheDocument();

// The recovered-as-fact overclaim must be absent while proof_events=0.
expect(container.textContent ?? "").not.toMatch(/we win back[^.]*lands here/i);
});

it("carries a DOM-measurable Sample marker for the honesty check", async () => {
stubFetchWith(makeLedger(0));
const { container } = render(<ProofLedger />);

await waitFor(() =>
expect(
container.querySelector('[data-proof-state="sample"]'),
).not.toBeNull(),
);
// A human-visible Sample chip too (not only a hidden attribute).
expect(screen.getByText(/^Sample$/i)).toBeInTheDocument();
});
});

describe("ProofLedger - LIVE state (real proof_events)", () => {
it("shows the present-tense recovered-revenue copy once proof_events exist", async () => {
stubFetchWith(makeLedger(1));
const { container } = render(<ProofLedger />);

await waitFor(() =>
expect(
container.querySelector('[data-proof-state="live"]'),
).not.toBeNull(),
);

// Present tense is earned once there is a real recovered dollar.
expect(container.textContent ?? "").toMatch(/we win back[^.]*lands here/i);
// And the readiness hedge / Sample chip must be gone.
expect(container.textContent ?? "").not.toMatch(/will land here/i);
expect(screen.queryByText(/^Sample$/i)).not.toBeInTheDocument();
});
});
30 changes: 26 additions & 4 deletions components/proof/ProofLedger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,38 @@ export function ProofLedger() {

if (state === "error") return null; // fail quiet; the rest of the page stands alone

// Honesty gate: the present-tense "lands here" claim asserts we ARE recovering
// dollars. That is only true once a real proof_event exists (First Light). At
// proof_events=0 (loading, empty, or Sample) we hedge to future tense and mark
// the surface as a Sample so a DOM/regex honesty check can assert readiness.
const hasRealProof = state === "ready" && !!ledger && ledger.totals.proof_events > 0;

return (
<section className="rounded-2xl border border-[var(--overlay-soft)] bg-[var(--overlay-subtle)] p-5 md:p-6">
<section
data-proof-state={hasRealProof ? "live" : "sample"}
className="rounded-2xl border border-[var(--overlay-soft)] bg-[var(--overlay-subtle)] p-5 md:p-6"
>
<div className="mb-4 flex items-start justify-between gap-3">
<div className="flex items-start gap-2">
<Receipt size={18} className="mt-0.5 shrink-0 text-amber-400" aria-hidden />
<div>
<h2 className="text-base font-bold text-stone-100">Your recovered-revenue record</h2>
<div className="flex items-center gap-2">
<h2 className="text-base font-bold text-stone-100">Your recovered-revenue record</h2>
{!hasRealProof ? (
<span
data-proof-state-chip="sample"
aria-label="Sample state, pending your first recovered result"
title="Sample: pending your first recovered result"
className="inline-flex shrink-0 items-center rounded-full border border-[var(--overlay-soft)] bg-[var(--overlay-subtle)] px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-stone-400"
>
Sample
</span>
) : null}
</div>
<p className="mt-1 text-sm text-stone-400">
Every dollar we win back lands here, owner-approved and timestamped. Yours to keep,
export, and show your accountant. A record no CRM can hand you.
{hasRealProof
? "Every dollar we win back lands here, owner-approved and timestamped. Yours to keep, export, and show your accountant. A record no CRM can hand you."
: "Every dollar we win back will land here, owner-approved and timestamped. Yours to keep, export, and show your accountant. A record no CRM can hand you."}
</p>
</div>
</div>
Expand Down