Skip to content
Merged
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
8 changes: 8 additions & 0 deletions comebackhere-frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,14 @@ h3 {
color: var(--color-success);
}

/* OnHold — visually distinct from Pending so payers understand the invoice
is blocked by an external hold rather than simply awaiting payment. */
.badge--on-hold {
background: var(--color-warning-bg);
color: var(--color-warning-text);
border: 1px solid #fcd34d;
}

.message {
padding: 12px 16px;
border-radius: var(--radius);
Expand Down
15 changes: 14 additions & 1 deletion comebackhere-frontend/src/components/StatusBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,23 @@ const statusColors: Record<string, string> = {
Cancelled: "badge badge--cancelled",
RefundRequested: "badge badge--refund-requested",
Released: "badge badge--released",
OnHold: "badge badge--on-hold",
}

/** Human-readable label for each status, so "OnHold" shows as "On Hold". */
const statusLabels: Record<string, string> = {
Pending: "Pending",
Paid: "Paid",
Expired: "Expired",
Cancelled: "Cancelled",
RefundRequested: "Refund Requested",
Released: "Released",
OnHold: "On Hold",
}

export function StatusBadge({ status }: StatusBadgeProps) {
const label = statusLabels[status] ?? status
return (
<span className={statusColors[status] ?? "badge"} role="status" aria-label={`Invoice status: ${status}`}>{status}</span>
<span className={statusColors[status] ?? "badge"} role="status" aria-label={`Invoice status: ${label}`}>{label}</span>
)
}
77 changes: 77 additions & 0 deletions comebackhere-frontend/src/tests/StatusBadge.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, it, expect } from 'vitest'
import '@testing-library/jest-dom'
import { render, screen } from '@testing-library/react'
import { StatusBadge } from '../components/StatusBadge'
import { InvoiceStatus } from '../types'

describe('StatusBadge', () => {
it('renders Pending badge', () => {
render(<StatusBadge status={InvoiceStatus.Pending} />)
const badge = screen.getByRole('status')
expect(badge).toHaveTextContent('Pending')
expect(badge).toHaveClass('badge--pending')
})

it('renders Paid badge', () => {
render(<StatusBadge status={InvoiceStatus.Paid} />)
const badge = screen.getByRole('status')
expect(badge).toHaveTextContent('Paid')
expect(badge).toHaveClass('badge--paid')
})

it('renders Expired badge', () => {
render(<StatusBadge status={InvoiceStatus.Expired} />)
const badge = screen.getByRole('status')
expect(badge).toHaveTextContent('Expired')
expect(badge).toHaveClass('badge--expired')
})

it('renders Cancelled badge', () => {
render(<StatusBadge status={InvoiceStatus.Cancelled} />)
const badge = screen.getByRole('status')
expect(badge).toHaveTextContent('Cancelled')
expect(badge).toHaveClass('badge--cancelled')
})

it('renders RefundRequested badge', () => {
render(<StatusBadge status={InvoiceStatus.RefundRequested} />)
const badge = screen.getByRole('status')
expect(badge).toHaveTextContent('Refund Requested')
expect(badge).toHaveClass('badge--refund-requested')
})

it('renders Released badge', () => {
render(<StatusBadge status={InvoiceStatus.Released} />)
const badge = screen.getByRole('status')
expect(badge).toHaveTextContent('Released')
expect(badge).toHaveClass('badge--released')
})

describe('OnHold variant (issue #258)', () => {
it('renders OnHold badge with distinct css class', () => {
render(<StatusBadge status={InvoiceStatus.OnHold} />)
const badge = screen.getByRole('status')
expect(badge).toHaveClass('badge--on-hold')
})

it('renders "On Hold" human-readable label, not "OnHold"', () => {
render(<StatusBadge status={InvoiceStatus.OnHold} />)
expect(screen.getByRole('status')).toHaveTextContent('On Hold')
})

it('OnHold and Pending use different CSS classes', () => {
const { rerender } = render(<StatusBadge status={InvoiceStatus.Pending} />)
const pendingClass = screen.getByRole('status').className

rerender(<StatusBadge status={InvoiceStatus.OnHold} />)
const onHoldClass = screen.getByRole('status').className

expect(onHoldClass).not.toBe(pendingClass)
})

it('has an accessible aria-label for screen readers', () => {
render(<StatusBadge status={InvoiceStatus.OnHold} />)
expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Invoice status: On Hold')
})
})
})
2 changes: 2 additions & 0 deletions comebackhere-frontend/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export enum InvoiceStatus {
Cancelled = "Cancelled",
RefundRequested = "RefundRequested",
Released = "Released",
/** Invoice is blocked because its underlying settlement is on hold. */
OnHold = "OnHold",
}

export type TransactionEventType =
Expand Down
Loading