Skip to content

Commit 7ab893e

Browse files
committed
fix(brex): reject whitespace-only expense IDs in receipt upload instead of silently falling back to receipt match
1 parent e563a9b commit 7ab893e

3 files changed

Lines changed: 28 additions & 6 deletions

File tree

apps/sim/app/api/tools/brex/upload-receipt/route.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,29 @@ describe('POST /api/tools/brex/upload-receipt', () => {
9494
expect(uploadInit.method).toBe('PUT')
9595
})
9696

97+
it('rejects a whitespace-only expense ID instead of falling back to receipt match', async () => {
98+
const response = await POST(createMockRequest('POST', { ...baseBody, expenseId: ' ' }))
99+
expect(response.status).toBe(400)
100+
expect(mockFetch).not.toHaveBeenCalled()
101+
})
102+
103+
it('trims a padded expense ID before building the upload URL', async () => {
104+
mockFetch
105+
.mockResolvedValueOnce(
106+
jsonResponse({ id: 'receipt_5', uri: 'https://s3.example.com/presigned' })
107+
)
108+
.mockResolvedValueOnce(jsonResponse({}))
109+
110+
const response = await POST(
111+
createMockRequest('POST', { ...baseBody, expenseId: ' expense_123 ' })
112+
)
113+
expect(response.status).toBe(200)
114+
const [createUrl] = mockFetch.mock.calls[0]
115+
expect(createUrl).toBe('https://api.brex.com/v1/expenses/card/expense_123/receipt_upload')
116+
const data = await response.json()
117+
expect(data.output.expenseId).toBe('expense_123')
118+
})
119+
97120
it('uses receipt match when no expense ID is provided', async () => {
98121
mockFetch
99122
.mockResolvedValueOnce(

apps/sim/app/api/tools/brex/upload-receipt/route.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
5353
}
5454

5555
const effectiveReceiptName = receiptName?.trim() || userFile.name
56-
const trimmedExpenseId = expenseId?.trim() || undefined
57-
const endpoint = trimmedExpenseId
58-
? `${BREX_API_BASE}/v1/expenses/card/${encodeURIComponent(trimmedExpenseId)}/receipt_upload`
56+
const endpoint = expenseId
57+
? `${BREX_API_BASE}/v1/expenses/card/${encodeURIComponent(expenseId)}/receipt_upload`
5958
: `${BREX_API_BASE}/v1/expenses/card/receipt_match`
6059

6160
logger.info(
62-
`[${requestId}] Creating Brex ${trimmedExpenseId ? 'receipt upload' : 'receipt match'}: ${effectiveReceiptName} (${fileBuffer.length} bytes)`
61+
`[${requestId}] Creating Brex ${expenseId ? 'receipt upload' : 'receipt match'}: ${effectiveReceiptName} (${fileBuffer.length} bytes)`
6362
)
6463

6564
const createResponse = await fetch(endpoint, {
@@ -116,7 +115,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
116115
output: {
117116
receiptId: createData.id,
118117
receiptName: effectiveReceiptName,
119-
expenseId: trimmedExpenseId ?? null,
118+
expenseId: expenseId ?? null,
120119
},
121120
})
122121
} catch (error) {

apps/sim/lib/api/contracts/tools/brex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
55

66
export const brexUploadReceiptBodySchema = z.object({
77
apiKey: z.string().min(1, 'API key is required'),
8-
expenseId: z.string().min(1, 'Expense ID cannot be empty').optional(),
8+
expenseId: z.string().trim().min(1, 'Expense ID cannot be empty').optional(),
99
file: RawFileInputSchema,
1010
receiptName: z
1111
.string()

0 commit comments

Comments
 (0)