Skip to content

Commit 5261c72

Browse files
committed
feat(billing): show invoice descriptions and cap in-app list at 5
1 parent b2d9c02 commit 5261c72

4 files changed

Lines changed: 51 additions & 18 deletions

File tree

apps/sim/app/api/billing/invoices/route.test.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe('GET /api/billing/invoices', () => {
4848
})
4949

5050
it('does not surface hasMore when the trailing raw invoice beyond MAX_INVOICES is a draft', async () => {
51-
const finalized = Array.from({ length: 10 }, () => makeInvoice())
51+
const finalized = Array.from({ length: 5 }, () => makeInvoice())
5252
mockStripeInvoicesList.mockResolvedValueOnce({
5353
data: [...finalized, makeInvoice({ status: 'draft' })],
5454
has_more: false,
@@ -58,22 +58,44 @@ describe('GET /api/billing/invoices', () => {
5858
const response = await GET(request)
5959
const body = await response.json()
6060

61-
expect(body.invoices).toHaveLength(10)
61+
expect(body.invoices).toHaveLength(5)
6262
expect(body.hasMore).toBe(false)
6363
})
6464

6565
it('reports hasMore when there are genuinely more finalized invoices', async () => {
66-
const finalized = Array.from({ length: 11 }, () => makeInvoice())
66+
const finalized = Array.from({ length: 6 }, () => makeInvoice())
6767
mockStripeInvoicesList.mockResolvedValueOnce({ data: finalized, has_more: false })
6868

6969
const request = createMockRequest('GET')
7070
const response = await GET(request)
7171
const body = await response.json()
7272

73-
expect(body.invoices).toHaveLength(10)
73+
expect(body.invoices).toHaveLength(5)
7474
expect(body.hasMore).toBe(true)
7575
})
7676

77+
it('surfaces the line-item description, preferring the top-level invoice description', async () => {
78+
mockStripeInvoicesList.mockResolvedValueOnce({
79+
data: [
80+
makeInvoice({ lines: { data: [{ description: 'Sim Max' }] } }),
81+
makeInvoice({
82+
description: 'Usage overage',
83+
lines: { data: [{ description: 'ignored line' }] },
84+
}),
85+
makeInvoice(),
86+
],
87+
has_more: false,
88+
})
89+
90+
const request = createMockRequest('GET')
91+
const response = await GET(request)
92+
const body = await response.json()
93+
94+
expect(body.invoices[0].description).toBe('Sim Max')
95+
expect(body.invoices[1].description).toBe('Usage overage')
96+
expect(body.invoices[2].description).toBeNull()
97+
})
98+
7799
it('pages through further drafts to confirm hasMore when the first page is inconclusive', async () => {
78100
const firstPage = Array.from({ length: 11 }, () => makeInvoice({ status: 'draft' }))
79101
mockStripeInvoicesList

apps/sim/app/api/billing/invoices/route.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1414

1515
const logger = createLogger('BillingInvoices')
1616

17-
/** Cap the number of invoices returned to the most recent statements. */
18-
const MAX_INVOICES = 10
17+
/** Cap the number of invoices returned to the most recent statements; the UI links out to Stripe's portal for the full history. */
18+
const MAX_INVOICES = 5
1919

2020
/** Stripe page size when scanning for finalized invoices; also bounds the has-more probe. */
2121
const STRIPE_PAGE_SIZE = MAX_INVOICES + 1
@@ -63,6 +63,7 @@ async function collectFinalizedInvoices(
6363
customer: stripeCustomerId,
6464
limit: STRIPE_PAGE_SIZE,
6565
starting_after: startingAfter,
66+
expand: ['data.lines'],
6667
})
6768

6869
invoices.push(
@@ -130,17 +131,21 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
130131
try {
131132
const finalized = await collectFinalizedInvoices(stripe, stripeCustomerId)
132133
const hasMore = finalized.invoices.length > MAX_INVOICES || finalized.stripeHasMore
133-
const invoices = finalized.invoices.slice(0, MAX_INVOICES).map((invoice) => ({
134-
id: invoice.id as string,
135-
number: invoice.number ?? null,
136-
created: invoice.created,
137-
total: invoice.total,
138-
amountPaid: invoice.amount_paid,
139-
currency: invoice.currency,
140-
status: invoice.status ?? null,
141-
hostedInvoiceUrl: invoice.hosted_invoice_url ?? null,
142-
invoicePdf: invoice.invoice_pdf ?? null,
143-
}))
134+
const invoices = finalized.invoices.slice(0, MAX_INVOICES).map((invoice) => {
135+
const lineDescription = invoice.lines?.data.find((line) => line.description)?.description
136+
return {
137+
id: invoice.id as string,
138+
number: invoice.number ?? null,
139+
created: invoice.created,
140+
total: invoice.total,
141+
amountPaid: invoice.amount_paid,
142+
currency: invoice.currency,
143+
status: invoice.status ?? null,
144+
description: invoice.description ?? lineDescription ?? null,
145+
hostedInvoiceUrl: invoice.hosted_invoice_url ?? null,
146+
invoicePdf: invoice.invoice_pdf ?? null,
147+
}
148+
})
144149

145150
return NextResponse.json({ success: true, invoices, hasMore })
146151
} catch (error) {

apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps
424424
id: invoice.id,
425425
date: formatDate(new Date(invoice.created * 1000)),
426426
amount: formatInvoiceAmount(invoice.total, invoice.currency),
427+
description: invoice.description,
427428
badge: getInvoiceStatusBadge(invoice.status),
428429
url: invoice.hostedInvoiceUrl ?? invoice.invoicePdf,
429430
}))
@@ -607,7 +608,7 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps
607608
'flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors'
608609
const rowContent = (
609610
<>
610-
<span className='min-w-0 flex-1 truncate text-[var(--text-body)] text-sm'>
611+
<span className='flex-shrink-0 text-[var(--text-body)] text-sm'>
611612
{invoice.date}
612613
</span>
613614
<Badge variant={invoice.badge.variant} size='sm'>
@@ -616,6 +617,9 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps
616617
<span className='flex-shrink-0 text-[var(--text-muted)] text-caption'>
617618
{invoice.amount}
618619
</span>
620+
<span className='min-w-0 flex-1 truncate text-[var(--text-muted)] text-caption'>
621+
{invoice.description ?? ''}
622+
</span>
619623
<ArrowRight className='size-4 flex-shrink-0 text-[var(--text-icon)]' />
620624
</>
621625
)

apps/sim/lib/api/contracts/subscription.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ export const invoiceItemSchema = z.object({
253253
amountPaid: z.number(),
254254
currency: z.string(),
255255
status: z.string().nullable(),
256+
/** Primary line-item / invoice description, e.g. "Usage overage" or the plan name. */
257+
description: z.string().nullable(),
256258
hostedInvoiceUrl: z.string().nullable(),
257259
invoicePdf: z.string().nullable(),
258260
})

0 commit comments

Comments
 (0)