diff --git a/mcpjam-inspector/client/src/components/billing/PaymentsHistorySection.tsx b/mcpjam-inspector/client/src/components/billing/PaymentsHistorySection.tsx
index fa2e7ee9a9..4b208cda52 100644
--- a/mcpjam-inspector/client/src/components/billing/PaymentsHistorySection.tsx
+++ b/mcpjam-inspector/client/src/components/billing/PaymentsHistorySection.tsx
@@ -24,8 +24,12 @@ import {
} from "@/hooks/usePaymentsHistory";
const formatUsd = (cents: number): string => {
- const dollars = cents / 100;
- return Number.isInteger(dollars) ? `$${dollars}` : `$${dollars.toFixed(2)}`;
+ const sign = cents < 0 ? "-" : "";
+ const dollars = Math.abs(cents) / 100;
+ const formatted = Number.isInteger(dollars)
+ ? `$${dollars}`
+ : `$${dollars.toFixed(2)}`;
+ return `${sign}${formatted}`;
};
const formatDate = (epochMs: number): string => {
@@ -110,6 +114,7 @@ function PaymentsTable({ entries }: { entries: PaymentHistoryEntry[] }) {
Date
Amount
Credits
+ Details
Status
Receipt
@@ -126,6 +131,9 @@ function PaymentsTable({ entries }: { entries: PaymentHistoryEntry[] }) {
{entry.displayCredits}
+
+ {entry.details}
+
@@ -159,6 +167,7 @@ function MobileRow({ entry }: { entry: PaymentHistoryEntry }) {
@@ -191,11 +200,33 @@ function StatusBadge({ entry }: { entry: PaymentHistoryEntry }) {
);
}
+ if (status === "pending_refund") {
+ return (
+
+
+ Refund pending
+
+ );
+ }
+ if (status === "credited" || status === "refunded_and_credited") {
+ return (
+
+
+ {status === "credited" ? "Credited" : "Refunded + credited"}
+
+ );
+ }
if (status === "refunded" || status === "partially_refunded") {
const isPartial = status === "partially_refunded";
// Hover detail like "$3 of $5 refunded" when we know the reversed amount.
const detail =
- typeof entry.reversedPaidCents === "number"
+ entry.pricePaidCents > 0 && typeof entry.reversedPaidCents === "number"
? `${formatUsd(entry.reversedPaidCents)} of ${formatUsd(
entry.pricePaidCents
)} refunded`
diff --git a/mcpjam-inspector/client/src/components/billing/__tests__/PaymentsHistorySection.test.tsx b/mcpjam-inspector/client/src/components/billing/__tests__/PaymentsHistorySection.test.tsx
index 8c82e8ef95..e0cd5ebde2 100644
--- a/mcpjam-inspector/client/src/components/billing/__tests__/PaymentsHistorySection.test.tsx
+++ b/mcpjam-inspector/client/src/components/billing/__tests__/PaymentsHistorySection.test.tsx
@@ -32,6 +32,7 @@ function makeEntry(
sessionId: overrides.sessionId ?? `cs_${overrides.id}`,
pricePaidCents: overrides.pricePaidCents ?? 1000,
displayCredits: overrides.displayCredits ?? "1,000 credits",
+ details: overrides.details ?? "Credit top-up",
status: overrides.status ?? "succeeded",
occurredAt: overrides.occurredAt ?? Date.now(),
...(overrides.reversedPaidCents !== undefined
@@ -249,5 +250,48 @@ describe("PaymentsHistorySection", () => {
expect(screen.getAllByText("Disputed")).toHaveLength(2);
expect(screen.queryByText("Succeeded")).not.toBeInTheDocument();
});
+
+ it("renders a seat refund row without a receipt link", () => {
+ renderWithStatus(
+ makeEntry({
+ id: "seat-refund",
+ sessionId: "seat-downgrade:sub:item:period:3->2",
+ pricePaidCents: -35997,
+ displayCredits: "-9,974 credits",
+ details: "-1 Team seat · prorated",
+ status: "refunded",
+ reversedPaidCents: 35997,
+ occurredAt: Date.UTC(2026, 5, 10),
+ })
+ );
+
+ expect(screen.getAllByText("-$359.97")).toHaveLength(2);
+ expect(screen.getAllByText("-1 Team seat · prorated")).toHaveLength(2);
+ expect(screen.getAllByText("Refunded")).toHaveLength(2);
+ expect(screen.queryByRole("link", { name: /View receipt/ })).toBeNull();
+ });
+
+ it("renders a split refund and credit fallback row", () => {
+ renderWithStatus(
+ makeEntry({
+ id: "seat-split",
+ sessionId: "seat-downgrade:sub:item:period:4->3",
+ pricePaidCents: -35997,
+ displayCredits: "-9,974 credits",
+ details:
+ "-1 Team seat · prorated · $300 card refund + $59.97 account credit",
+ status: "refunded_and_credited",
+ reversedPaidCents: 30000,
+ accountCreditedCents: 5997,
+ })
+ );
+
+ expect(screen.getAllByText("Refunded + credited")).toHaveLength(2);
+ expect(
+ screen.getAllByText(
+ "-1 Team seat · prorated · $300 card refund + $59.97 account credit"
+ )
+ ).toHaveLength(2);
+ });
});
});
diff --git a/mcpjam-inspector/client/src/hooks/__tests__/usePaymentsHistory.test.ts b/mcpjam-inspector/client/src/hooks/__tests__/usePaymentsHistory.test.ts
index e433762263..c1b32e3b65 100644
--- a/mcpjam-inspector/client/src/hooks/__tests__/usePaymentsHistory.test.ts
+++ b/mcpjam-inspector/client/src/hooks/__tests__/usePaymentsHistory.test.ts
@@ -79,6 +79,7 @@ describe("usePaymentsHistory", () => {
sessionId: "cs_1",
pricePaidCents: 2500,
displayCredits: "2,500 credits",
+ details: "Credit top-up",
status: "succeeded",
occurredAt: 100,
receiptUrl: "https://pay.stripe.com/receipts/abc",
@@ -102,6 +103,35 @@ describe("usePaymentsHistory", () => {
expect(result.current.entries?.[0].receiptUrl).toBeUndefined();
});
+ it("normalizes a seat refund row with account credit fallback fields", () => {
+ queryReturn = {
+ items: [
+ {
+ id: "seat_refund_1",
+ sessionId: "seat-downgrade:sub:item:period:3->2",
+ pricePaidCents: -35997,
+ displayCredits: "-9,974 credits",
+ details:
+ "-1 Team seat · prorated · $300 card refund + $59.97 account credit",
+ status: "refunded_and_credited",
+ occurredAt: 300,
+ reversedPaidCents: 30000,
+ accountCreditedCents: 5997,
+ },
+ ],
+ };
+ const { result } = renderHook(() => usePaymentsHistory(ORG_ID));
+ expect(result.current.entries).toHaveLength(1);
+ expect(result.current.entries?.[0]).toMatchObject({
+ id: "seat_refund_1",
+ pricePaidCents: -35997,
+ details:
+ "-1 Team seat · prorated · $300 card refund + $59.97 account credit",
+ status: "refunded_and_credited",
+ accountCreditedCents: 5997,
+ });
+ });
+
it("drops malformed rows but keeps valid neighbors", () => {
queryReturn = {
items: [
diff --git a/mcpjam-inspector/client/src/hooks/usePaymentsHistory.ts b/mcpjam-inspector/client/src/hooks/usePaymentsHistory.ts
index ac078f8a7f..6a7d5eaea0 100644
--- a/mcpjam-inspector/client/src/hooks/usePaymentsHistory.ts
+++ b/mcpjam-inspector/client/src/hooks/usePaymentsHistory.ts
@@ -8,15 +8,20 @@ export type PaymentHistoryStatus =
| "failed"
| "refunded"
| "partially_refunded"
- | "disputed";
+ | "disputed"
+ | "credited"
+ | "refunded_and_credited"
+ | "pending_refund";
export interface PaymentHistoryEntry {
id: string;
sessionId: string;
pricePaidCents: number;
displayCredits: string;
+ details: string;
/** Paid cents handed back when refunded/charged back. Reversed rows only. */
reversedPaidCents?: number;
+ accountCreditedCents?: number;
status: PaymentHistoryStatus;
occurredAt: number;
receiptUrl?: string;
@@ -27,7 +32,9 @@ interface RawEntry {
sessionId?: unknown;
pricePaidCents?: unknown;
displayCredits?: unknown;
+ details?: unknown;
reversedPaidCents?: unknown;
+ accountCreditedCents?: unknown;
status?: unknown;
occurredAt?: unknown;
receiptUrl?: unknown;
@@ -40,6 +47,9 @@ const VALID_STATUSES = new Set
([
"refunded",
"partially_refunded",
"disputed",
+ "credited",
+ "refunded_and_credited",
+ "pending_refund",
]);
const isValidStatus = (value: unknown): value is PaymentHistoryStatus =>
@@ -73,11 +83,16 @@ const normalize = (raw: unknown): PaymentHistoryEntry[] | undefined => {
sessionId: item.sessionId,
pricePaidCents: item.pricePaidCents,
displayCredits: item.displayCredits,
+ details:
+ typeof item.details === "string" ? item.details : "Credit top-up",
status: item.status,
occurredAt: item.occurredAt,
...(typeof item.reversedPaidCents === "number"
? { reversedPaidCents: item.reversedPaidCents }
: {}),
+ ...(typeof item.accountCreditedCents === "number"
+ ? { accountCreditedCents: item.accountCreditedCents }
+ : {}),
...(typeof item.receiptUrl === "string" && item.receiptUrl.length > 0
? { receiptUrl: item.receiptUrl }
: {}),