From e563a9bbe4693b00fac8e13fcd0b8aa68022ee02 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 16:45:26 -0700 Subject: [PATCH 1/7] feat(integrations): add Brex integration with expenses, receipts, transactions, team, budgets, and payments tools --- apps/docs/components/icons.tsx | 11 + apps/docs/components/ui/icon-mapping.ts | 2 + .../content/docs/en/integrations/brex.mdx | 879 ++++++++++++++++++ .../content/docs/en/integrations/meta.json | 1 + .../tools/brex/upload-receipt/route.test.ts | 181 ++++ .../api/tools/brex/upload-receipt/route.ts | 129 +++ apps/sim/blocks/blocks/brex.ts | 671 +++++++++++++ apps/sim/blocks/registry.ts | 3 + apps/sim/components/icons.tsx | 11 + apps/sim/lib/api/contracts/tools/brex.ts | 34 + apps/sim/lib/integrations/icon-mapping.ts | 2 + apps/sim/lib/integrations/integrations.json | 131 +++ apps/sim/tools/brex/get_budget.ts | 79 ++ apps/sim/tools/brex/get_cash_account.ts | 78 ++ apps/sim/tools/brex/get_company.ts | 53 ++ apps/sim/tools/brex/get_current_user.ts | 59 ++ apps/sim/tools/brex/get_expense.ts | 199 ++++ apps/sim/tools/brex/get_spend_limit.ts | 89 ++ apps/sim/tools/brex/get_transfer.ts | 100 ++ apps/sim/tools/brex/get_user.ts | 65 ++ apps/sim/tools/brex/get_vendor.ts | 56 ++ apps/sim/tools/brex/index.ts | 28 + apps/sim/tools/brex/list_budgets.ts | 98 ++ apps/sim/tools/brex/list_card_accounts.ts | 73 ++ apps/sim/tools/brex/list_card_statements.ts | 95 ++ apps/sim/tools/brex/list_card_transactions.ts | 93 ++ apps/sim/tools/brex/list_cards.ts | 101 ++ apps/sim/tools/brex/list_cash_accounts.ts | 99 ++ apps/sim/tools/brex/list_cash_statements.ts | 100 ++ apps/sim/tools/brex/list_cash_transactions.ts | 92 ++ apps/sim/tools/brex/list_departments.ts | 90 ++ apps/sim/tools/brex/list_expenses.ts | 122 +++ apps/sim/tools/brex/list_locations.ts | 87 ++ apps/sim/tools/brex/list_spend_limits.ts | 108 +++ apps/sim/tools/brex/list_titles.ts | 86 ++ apps/sim/tools/brex/list_transfers.ts | 125 +++ apps/sim/tools/brex/list_users.ts | 79 ++ apps/sim/tools/brex/list_vendors.ts | 93 ++ apps/sim/tools/brex/match_receipt.ts | 62 ++ apps/sim/tools/brex/types.ts | 749 +++++++++++++++ apps/sim/tools/brex/update_expense.ts | 95 ++ apps/sim/tools/brex/upload_receipt.ts | 70 ++ apps/sim/tools/brex/utils.ts | 52 ++ apps/sim/tools/registry.ts | 58 ++ 44 files changed, 5488 insertions(+) create mode 100644 apps/docs/content/docs/en/integrations/brex.mdx create mode 100644 apps/sim/app/api/tools/brex/upload-receipt/route.test.ts create mode 100644 apps/sim/app/api/tools/brex/upload-receipt/route.ts create mode 100644 apps/sim/blocks/blocks/brex.ts create mode 100644 apps/sim/lib/api/contracts/tools/brex.ts create mode 100644 apps/sim/tools/brex/get_budget.ts create mode 100644 apps/sim/tools/brex/get_cash_account.ts create mode 100644 apps/sim/tools/brex/get_company.ts create mode 100644 apps/sim/tools/brex/get_current_user.ts create mode 100644 apps/sim/tools/brex/get_expense.ts create mode 100644 apps/sim/tools/brex/get_spend_limit.ts create mode 100644 apps/sim/tools/brex/get_transfer.ts create mode 100644 apps/sim/tools/brex/get_user.ts create mode 100644 apps/sim/tools/brex/get_vendor.ts create mode 100644 apps/sim/tools/brex/index.ts create mode 100644 apps/sim/tools/brex/list_budgets.ts create mode 100644 apps/sim/tools/brex/list_card_accounts.ts create mode 100644 apps/sim/tools/brex/list_card_statements.ts create mode 100644 apps/sim/tools/brex/list_card_transactions.ts create mode 100644 apps/sim/tools/brex/list_cards.ts create mode 100644 apps/sim/tools/brex/list_cash_accounts.ts create mode 100644 apps/sim/tools/brex/list_cash_statements.ts create mode 100644 apps/sim/tools/brex/list_cash_transactions.ts create mode 100644 apps/sim/tools/brex/list_departments.ts create mode 100644 apps/sim/tools/brex/list_expenses.ts create mode 100644 apps/sim/tools/brex/list_locations.ts create mode 100644 apps/sim/tools/brex/list_spend_limits.ts create mode 100644 apps/sim/tools/brex/list_titles.ts create mode 100644 apps/sim/tools/brex/list_transfers.ts create mode 100644 apps/sim/tools/brex/list_users.ts create mode 100644 apps/sim/tools/brex/list_vendors.ts create mode 100644 apps/sim/tools/brex/match_receipt.ts create mode 100644 apps/sim/tools/brex/types.ts create mode 100644 apps/sim/tools/brex/update_expense.ts create mode 100644 apps/sim/tools/brex/upload_receipt.ts create mode 100644 apps/sim/tools/brex/utils.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 6161069bef..90987f14fd 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -2261,6 +2261,17 @@ export function BrandfetchIcon(props: SVGProps) { ) } +export function BrexIcon(props: SVGProps) { + return ( + + + + ) +} + export function BrightDataIcon(props: SVGProps) { return ( = { azure_devops: AzureIcon, box: BoxCompanyIcon, brandfetch: BrandfetchIcon, + brex: BrexIcon, brightdata: BrightDataIcon, browser_use: BrowserUseIcon, calcom: CalComIcon, diff --git a/apps/docs/content/docs/en/integrations/brex.mdx b/apps/docs/content/docs/en/integrations/brex.mdx new file mode 100644 index 0000000000..54d4ab6d14 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/brex.mdx @@ -0,0 +1,879 @@ +--- +title: Brex +description: Manage expenses, receipts, transactions, and team data in Brex +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Brex](https://www.brex.com/) is the AI-powered spend platform that gives companies corporate cards, expense management, banking, and bill pay in one place. Finance teams use Brex to control spend with budgets and spend limits, automate expense review, and keep every transaction reconciled with receipts and memos. + +With the Brex integration in Sim, your agents can work directly with your company's spend data: + +- **Expenses**: List and filter expenses by status, owner, or purchase date, fetch full expense details (merchant, amounts, receipts), and update expense memos. +- **Receipts**: Upload a receipt file straight onto a specific card expense, or let Brex automatically match an uploaded receipt to the right expense. +- **Transactions and accounts**: Pull settled card transactions, cash account transactions, account balances, and finalized statements for reporting and reconciliation. +- **Budgets and spend limits**: Read budgets and spend limits — including current period balances — to power utilization reports and proactive alerts. +- **Team**: Look up users, departments, locations, titles, and cards to enrich spend data with organizational context. +- **Payments**: Track vendors and money transfers to monitor payment status end to end. + +Authentication uses a Brex user token, which you can generate from **Developer → Settings** in your Brex dashboard. The integration is intentionally read-focused: it never moves money, issues cards, or exposes card numbers. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrates Brex into the workflow. List and update expenses, upload and match receipts, view card and cash transactions, accounts, budgets, spend limits, vendors, transfers, and team data. + + + +## Actions + +### `brex_list_expenses` + +List expenses in the Brex account with optional filters for user, status, payment status, and purchase date range + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `userIds` | string | No | Comma-separated user IDs to filter expenses by owner | +| `statuses` | string | No | Comma-separated expense statuses to filter by: DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED | +| `paymentStatuses` | string | No | Comma-separated payment statuses to filter by: NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED | +| `purchasedAtStart` | string | No | Only include expenses purchased at or after this ISO 8601 timestamp | +| `purchasedAtEnd` | string | No | Only include expenses purchased before this ISO 8601 timestamp | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of expenses to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Expenses matching the filters | +| ↳ `id` | string | Unique expense ID | +| ↳ `memo` | string | Memo on the expense | +| ↳ `status` | string | Expense status \(DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED\) | +| ↳ `payment_status` | string | Payment status \(NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED\) | +| ↳ `expense_type` | string | Expense type \(CARD, BILLPAY, REIMBURSEMENT, CLAWBACK, UNSET\) | +| ↳ `category` | string | Merchant category of the expense | +| ↳ `merchant` | json | Merchant details | +| ↳ `raw_descriptor` | string | Raw merchant descriptor | +| ↳ `mcc` | string | Merchant category code | +| ↳ `country` | string | Merchant country | +| ↳ `user` | json | User who made the expense | +| ↳ `id` | string | User ID | +| ↳ `first_name` | string | First name | +| ↳ `last_name` | string | Last name | +| ↳ `budget` | json | Budget the expense belongs to | +| ↳ `id` | string | Budget ID | +| ↳ `name` | string | Budget name | +| ↳ `department` | json | Department of the expense owner | +| ↳ `id` | string | Department ID | +| ↳ `name` | string | Department name | +| ↳ `location` | json | Location of the expense owner | +| ↳ `id` | string | Location ID | +| ↳ `name` | string | Location name | +| ↳ `original_amount` | json | Original transaction amount | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `billing_amount` | json | Amount billed to the account | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `purchased_amount` | json | Amount at the time of purchase | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `receipts` | array | Receipts attached to the expense | +| ↳ `id` | string | Receipt ID | +| ↳ `download_uris` | array | Pre-signed receipt download URLs | +| ↳ `purchased_at` | string | Purchase timestamp \(ISO 8601\) | +| ↳ `updated_at` | string | Last update timestamp \(ISO 8601\) | +| ↳ `dashboard_url` | string | Link to the expense in the Brex dashboard | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_get_expense` + +Get a single Brex expense by its ID, including merchant, user, and receipt details + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `expenseId` | string | Yes | ID of the expense to fetch | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Unique expense ID | +| `memo` | string | Memo on the expense | +| `status` | string | Expense status \(DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED\) | +| `paymentStatus` | string | Payment status \(NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED\) | +| `expenseType` | string | Expense type \(CARD, BILLPAY, REIMBURSEMENT, CLAWBACK, UNSET\) | +| `category` | string | Merchant category of the expense | +| `merchantId` | string | Merchant ID | +| `merchant` | json | Merchant details \(raw descriptor, MCC, country\) | +| ↳ `raw_descriptor` | string | Raw merchant descriptor | +| ↳ `mcc` | string | Merchant category code | +| ↳ `country` | string | Merchant country | +| `budgetId` | string | Budget ID | +| `budget` | json | Budget the expense belongs to | +| ↳ `id` | string | Budget ID | +| ↳ `name` | string | Budget name | +| `departmentId` | string | Department ID | +| `department` | json | Department of the expense owner | +| ↳ `id` | string | Department ID | +| ↳ `name` | string | Department name | +| `locationId` | string | Location ID | +| `location` | json | Location of the expense owner | +| ↳ `id` | string | Location ID | +| ↳ `name` | string | Location name | +| `userId` | string | ID of the user who made the expense | +| `user` | json | User who made the expense | +| ↳ `id` | string | User ID | +| ↳ `first_name` | string | First name | +| ↳ `last_name` | string | Last name | +| `originalAmount` | json | Original transaction amount | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `billingAmount` | json | Amount billed to the account | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `purchasedAmount` | json | Amount at the time of purchase | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `usdEquivalentAmount` | json | USD equivalent amount | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `purchasedAt` | string | Purchase timestamp \(ISO 8601\) | +| `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| `paymentPostedAt` | string | Timestamp the payment was posted \(ISO 8601\) | +| `receipts` | array | Receipts attached to the expense | +| ↳ `id` | string | Receipt ID | +| ↳ `download_uris` | array | Pre-signed receipt download URLs | +| `dashboardUrl` | string | Link to the expense in the Brex dashboard | + +### `brex_update_expense` + +Update the memo of a Brex card expense + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `expenseId` | string | Yes | ID of the card expense to update | +| `memo` | string | Yes | New memo for the expense | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Unique expense ID | +| `memo` | string | Updated memo on the expense | +| `status` | string | Expense status \(DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED\) | +| `paymentStatus` | string | Payment status \(NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED\) | +| `category` | string | Merchant category of the expense | +| `merchantId` | string | Merchant ID | +| `budgetId` | string | Budget ID | +| `originalAmount` | json | Original transaction amount | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `billingAmount` | json | Amount billed to the account | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `purchasedAt` | string | Purchase timestamp \(ISO 8601\) | +| `updatedAt` | string | Last update timestamp \(ISO 8601\) | + +### `brex_upload_receipt` + +Upload a receipt file and attach it to a specific Brex card expense + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `expenseId` | string | Yes | ID of the card expense to attach the receipt to | +| `file` | file | Yes | Receipt file to upload \(max 50 MB\) | +| `receiptName` | string | No | Receipt file name including extension \(defaults to the uploaded file name\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `receiptId` | string | Unique identifier of the receipt upload | +| `receiptName` | string | Name the receipt was uploaded with | +| `expenseId` | string | ID of the expense the receipt was attached to | + +### `brex_match_receipt` + +Upload a receipt file and let Brex automatically match it with existing expenses + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `file` | file | Yes | Receipt file to upload \(max 50 MB\) | +| `receiptName` | string | No | Receipt file name including extension \(defaults to the uploaded file name\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `receiptId` | string | Unique identifier of the receipt match request | +| `receiptName` | string | Name the receipt was uploaded with | +| `expenseId` | string | Always null for receipt match \(Brex matches the receipt asynchronously\) | + +### `brex_list_card_transactions` + +List settled card transactions for all Brex card accounts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `userIds` | string | No | Comma-separated user IDs to filter transactions by cardholder | +| `postedAtStart` | string | No | Only include transactions posted at or after this ISO 8601 timestamp | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of transactions to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Settled card transactions | +| ↳ `id` | string | Unique transaction ID | +| ↳ `card_id` | string | ID of the card used | +| ↳ `description` | string | Transaction description | +| ↳ `amount` | json | Transaction amount | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `initiated_at_date` | string | Date the transaction was initiated | +| ↳ `posted_at_date` | string | Date the transaction was posted | +| ↳ `type` | string | Transaction type \(PURCHASE, REFUND, CHARGEBACK, REWARDS_CREDIT, COLLECTION, BNPL_FEE\) | +| ↳ `merchant` | json | Merchant details | +| ↳ `raw_descriptor` | string | Raw merchant descriptor | +| ↳ `mcc` | string | Merchant category code | +| ↳ `country` | string | Merchant country | +| ↳ `expense_id` | string | Associated expense ID | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_list_cash_transactions` + +List transactions for a Brex cash account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `accountId` | string | Yes | ID of the cash account to list transactions for | +| `postedAtStart` | string | No | Only include transactions posted at or after this ISO 8601 timestamp | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of transactions to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Cash account transactions | +| ↳ `id` | string | Unique transaction ID | +| ↳ `description` | string | Transaction description | +| ↳ `amount` | json | Transaction amount | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `initiated_at_date` | string | Date the transaction was initiated | +| ↳ `posted_at_date` | string | Date the transaction was posted | +| ↳ `type` | string | Transaction type | +| ↳ `transfer_id` | string | Associated transfer ID | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_list_card_accounts` + +List all Brex card accounts with balances and limits + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `accounts` | array | Card accounts | +| ↳ `id` | string | Unique account ID | +| ↳ `status` | string | Account status | +| ↳ `current_balance` | json | Current balance | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `available_balance` | json | Available balance | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `account_limit` | json | Account limit | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `current_statement_period` | json | Current statement period \(start_date, end_date\) | + +### `brex_list_cash_accounts` + +List all Brex cash accounts with balances and account details + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of accounts to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Cash accounts | +| ↳ `id` | string | Unique account ID | +| ↳ `name` | string | Account name | +| ↳ `status` | string | Account status | +| ↳ `current_balance` | json | Current balance | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `available_balance` | json | Available balance | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `account_number` | string | Bank account number | +| ↳ `routing_number` | string | Bank routing number | +| ↳ `primary` | boolean | Whether this is the primary cash account | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_get_cash_account` + +Get a Brex cash account by ID, or the primary cash account when no ID is provided + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `accountId` | string | No | ID of the cash account \(defaults to the primary cash account\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Unique account ID | +| `name` | string | Account name | +| `status` | string | Account status | +| `currentBalance` | json | Current balance | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `availableBalance` | json | Available balance | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `accountNumber` | string | Bank account number | +| `routingNumber` | string | Bank routing number | +| `primary` | boolean | Whether this is the primary cash account | + +### `brex_list_card_statements` + +List finalized statements for the primary Brex card account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of statements to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Finalized card account statements | +| ↳ `id` | string | Unique statement ID | +| ↳ `start_balance` | json | Balance at the start of the period | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `end_balance` | json | Balance at the end of the period | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `period` | json | Statement period \(start_date, end_date\) | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_list_cash_statements` + +List finalized statements for a Brex cash account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `accountId` | string | Yes | ID of the cash account to list statements for | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of statements to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Finalized cash account statements | +| ↳ `id` | string | Unique statement ID | +| ↳ `start_balance` | json | Balance at the start of the period | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `end_balance` | json | Balance at the end of the period | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `period` | json | Statement period \(start_date, end_date\) | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_list_users` + +List users in the Brex account, optionally filtered by email + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `email` | string | No | Filter users by exact email address | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of users to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Users in the Brex account | +| ↳ `id` | string | Unique user ID | +| ↳ `first_name` | string | First name | +| ↳ `last_name` | string | Last name | +| ↳ `email` | string | Email address | +| ↳ `status` | string | User status \(e.g., INVITED, ACTIVE, CLOSED, DISABLED\) | +| ↳ `manager_id` | string | ID of the manager | +| ↳ `department_id` | string | Department ID | +| ↳ `location_id` | string | Location ID | +| ↳ `title_id` | string | Title ID | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_get_user` + +Get a Brex user by their ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `userId` | string | Yes | ID of the user to fetch | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Unique user ID | +| `firstName` | string | First name | +| `lastName` | string | Last name | +| `email` | string | Email address | +| `status` | string | User status \(e.g., INVITED, ACTIVE, CLOSED, DISABLED\) | +| `managerId` | string | ID of the manager | +| `departmentId` | string | Department ID | +| `locationId` | string | Location ID | +| `titleId` | string | Title ID | + +### `brex_get_current_user` + +Get the Brex user associated with the API token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Unique user ID | +| `firstName` | string | First name | +| `lastName` | string | Last name | +| `email` | string | Email address | +| `status` | string | User status \(e.g., INVITED, ACTIVE, CLOSED, DISABLED\) | +| `managerId` | string | ID of the manager | +| `departmentId` | string | Department ID | +| `locationId` | string | Location ID | +| `titleId` | string | Title ID | + +### `brex_list_departments` + +List departments in the Brex account, optionally filtered by name + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `name` | string | No | Filter departments by name | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of departments to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Departments in the Brex account | +| ↳ `id` | string | Unique department ID | +| ↳ `name` | string | Department name | +| ↳ `description` | string | Department description | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_list_locations` + +List locations in the Brex account, optionally filtered by name + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `name` | string | No | Filter locations by name | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of locations to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Locations in the Brex account | +| ↳ `id` | string | Unique location ID | +| ↳ `name` | string | Location name | +| ↳ `description` | string | Location description | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_list_titles` + +List job titles in the Brex account, optionally filtered by name + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `name` | string | No | Filter titles by name | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of titles to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Job titles in the Brex account | +| ↳ `id` | string | Unique title ID | +| ↳ `name` | string | Title name | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_list_cards` + +List cards in the Brex account, optionally filtered by card owner + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `userId` | string | No | Filter cards by the ID of the card owner | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of cards to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Cards in the Brex account | +| ↳ `id` | string | Unique card ID | +| ↳ `owner` | json | Card owner \(type, user_id\) | +| ↳ `status` | string | Card status | +| ↳ `last_four` | string | Last four digits of the card number | +| ↳ `card_name` | string | Card name | +| ↳ `card_type` | string | Card type \(VIRTUAL or PHYSICAL\) | +| ↳ `limit_type` | string | Limit type \(CARD or USER\) | +| ↳ `spend_controls` | json | Spend controls on the card | +| ↳ `billing_address` | json | Billing address of the card | +| ↳ `expiration_date` | json | Card expiration date \(month, year\) | +| ↳ `budget_id` | string | Associated budget ID | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_get_company` + +Get the Brex company associated with the API token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Unique company ID | +| `legalName` | string | Legal name of the company | +| `mailingAddress` | json | Company mailing address \(line1, line2, city, state, country, postal_code\) | +| `accountType` | string | Brex account type \(BREX_CLASSIC or BREX_EMPOWER\) | + +### `brex_list_budgets` + +List budgets in the Brex account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of budgets to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Budgets in the Brex account | +| ↳ `budget_id` | string | Unique budget ID | +| ↳ `account_id` | string | Account ID the budget belongs to | +| ↳ `name` | string | Budget name | +| ↳ `description` | string | Budget description | +| ↳ `parent_budget_id` | string | Parent budget ID | +| ↳ `owner_user_ids` | array | User IDs of the budget owners | +| ↳ `period_recurrence_type` | string | Budget period recurrence \(e.g., MONTHLY, QUARTERLY, YEARLY, ONE_TIME\) | +| ↳ `start_date` | string | Budget start date | +| ↳ `end_date` | string | Budget end date | +| ↳ `amount` | json | Budget amount | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `spend_budget_status` | string | Budget status | +| ↳ `limit_type` | string | Budget limit type | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_get_budget` + +Get a Brex budget by its ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `budgetId` | string | Yes | ID of the budget to fetch | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `budgetId` | string | Unique budget ID | +| `accountId` | string | Account ID the budget belongs to | +| `name` | string | Budget name | +| `description` | string | Budget description | +| `parentBudgetId` | string | Parent budget ID | +| `ownerUserIds` | array | User IDs of the budget owners | +| `periodRecurrenceType` | string | Budget period recurrence \(WEEKLY, MONTHLY, QUARTERLY, YEARLY, ONE_TIME\) | +| `startDate` | string | Budget start date | +| `endDate` | string | Budget end date | +| `amount` | json | Budget amount | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `spendBudgetStatus` | string | Budget status \(ACTIVE, ARCHIVED, DELETED, EXPIRED\) | +| `limitType` | string | Budget limit type \(HARD or SOFT\) | + +### `brex_list_spend_limits` + +List spend limits in the Brex account, optionally filtered by member user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `memberUserIds` | string | No | Comma-separated user IDs to filter spend limits by member | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of spend limits to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Spend limits in the Brex account | +| ↳ `id` | string | Unique spend limit ID | +| ↳ `account_id` | string | Account ID the spend limit belongs to | +| ↳ `name` | string | Spend limit name | +| ↳ `description` | string | Spend limit description | +| ↳ `parent_budget_id` | string | Parent budget ID | +| ↳ `status` | string | Spend limit status | +| ↳ `period_recurrence_type` | string | Period recurrence \(e.g., MONTHLY, QUARTERLY, YEARLY, ONE_TIME\) | +| ↳ `spend_type` | string | Spend type of the limit | +| ↳ `owner_user_ids` | array | User IDs of the spend limit owners | +| ↳ `member_user_ids` | array | User IDs of the spend limit members | +| ↳ `current_period_balance` | json | Balance for the current period | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_get_spend_limit` + +Get a Brex spend limit by its ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `spendLimitId` | string | Yes | ID of the spend limit to fetch | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Unique spend limit ID | +| `accountId` | string | Account ID the spend limit belongs to | +| `name` | string | Spend limit name | +| `description` | string | Spend limit description | +| `parentBudgetId` | string | Parent budget ID | +| `status` | string | Spend limit status \(ACTIVE, EXPIRED, ARCHIVED, DELETED\) | +| `periodRecurrenceType` | string | Period recurrence \(PER_WEEK, PER_MONTH, PER_QUARTER, PER_YEAR, ONE_TIME\) | +| `spendType` | string | Spend type of the limit | +| `startDate` | string | Spend limit start date | +| `endDate` | string | Spend limit end date | +| `ownerUserIds` | array | User IDs of the spend limit owners | +| `memberUserIds` | array | User IDs of the spend limit members | +| `currentPeriodBalance` | json | Balance for the current period | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `authorizationSettings` | json | Authorization settings \(base limit, authorization type, rollover refresh\) | + +### `brex_list_vendors` + +List vendors in the Brex account, optionally filtered by name + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `name` | string | No | Filter vendors by name | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of vendors to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Vendors in the Brex account | +| ↳ `id` | string | Unique vendor ID | +| ↳ `company_name` | string | Vendor company name | +| ↳ `email` | string | Vendor email address | +| ↳ `phone` | string | Vendor phone number | +| ↳ `payment_accounts` | array | Payment accounts associated with the vendor | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_get_vendor` + +Get a Brex vendor by its ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `vendorId` | string | Yes | ID of the vendor to fetch | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Unique vendor ID | +| `companyName` | string | Vendor company name | +| `email` | string | Vendor email address | +| `phone` | string | Vendor phone number | +| `paymentAccounts` | array | Payment accounts associated with the vendor | + +### `brex_list_transfers` + +List money transfers in the Brex account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `cursor` | string | No | Pagination cursor from a previous response | +| `limit` | string | No | Number of transfers to return \(default 100, max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Transfers in the Brex account | +| ↳ `id` | string | Unique transfer ID | +| ↳ `counterparty` | json | Transfer counterparty details | +| ↳ `description` | string | Transfer description | +| ↳ `payment_type` | string | Payment type \(e.g., ACH, DOMESTIC_WIRE, CHEQUE, INTERNATIONAL_WIRE\) | +| ↳ `amount` | json | Transfer amount | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `process_date` | string | Date the transfer processes | +| ↳ `originating_account` | json | Account the transfer originates from | +| ↳ `status` | string | Transfer status \(e.g., SCHEDULED, PROCESSING, COMPLETED, FAILED\) | +| ↳ `cancellation_reason` | string | Reason the transfer was canceled | +| ↳ `estimated_delivery_date` | string | Estimated delivery date | +| ↳ `creator_user_id` | string | ID of the user who created the transfer | +| ↳ `created_at` | string | Creation timestamp | +| ↳ `display_name` | string | Transfer display name | +| ↳ `external_memo` | string | External memo | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `brex_get_transfer` + +Get a Brex money transfer by its ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Brex user token \(generated from Developer Settings in the Brex dashboard\) | +| `transferId` | string | Yes | ID of the transfer to fetch | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Unique transfer ID | +| `counterparty` | json | Transfer counterparty details | +| `description` | string | Transfer description | +| `paymentType` | string | Payment type \(ACH, DOMESTIC_WIRE, CHEQUE, INTERNATIONAL_WIRE, BOOK_TRANSFER\) | +| `amount` | json | Transfer amount | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `processDate` | string | Date the transfer processes | +| `originatingAccount` | json | Account the transfer originates from | +| `status` | string | Transfer status \(PROCESSING, SCHEDULED, PENDING_APPROVAL, FAILED, PROCESSED\) | +| `cancellationReason` | string | Reason the transfer was canceled | +| `estimatedDeliveryDate` | string | Estimated delivery date | +| `creatorUserId` | string | ID of the user who created the transfer | +| `createdAt` | string | Creation timestamp | +| `displayName` | string | Transfer display name | +| `externalMemo` | string | External memo | + + diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json index d96c79dba7..14d9f23ab7 100644 --- a/apps/docs/content/docs/en/integrations/meta.json +++ b/apps/docs/content/docs/en/integrations/meta.json @@ -21,6 +21,7 @@ "azure_devops", "box", "brandfetch", + "brex", "brightdata", "browser_use", "calcom", diff --git a/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts b/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts new file mode 100644 index 0000000000..9791a1fd6a --- /dev/null +++ b/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts @@ -0,0 +1,181 @@ +/** + * @vitest-environment node + */ +import { createMockRequest, hybridAuthMockFns } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockProcessFilesToUserFiles, mockDownloadFileFromStorage, mockAssertToolFileAccess } = + vi.hoisted(() => ({ + mockProcessFilesToUserFiles: vi.fn(), + mockDownloadFileFromStorage: vi.fn(), + mockAssertToolFileAccess: vi.fn(), + })) + +vi.mock('@/lib/uploads/utils/file-utils', () => ({ + processFilesToUserFiles: mockProcessFilesToUserFiles, +})) +vi.mock('@/lib/uploads/utils/file-utils.server', () => ({ + downloadFileFromStorage: mockDownloadFileFromStorage, +})) +vi.mock('@/app/api/files/authorization', () => ({ + assertToolFileAccess: mockAssertToolFileAccess, +})) + +import { POST } from '@/app/api/tools/brex/upload-receipt/route' + +const mockFetch = vi.fn() + +const baseBody = { + apiKey: 'bxt_test_token', + expenseId: 'expense_123', + file: { key: 'uploads/receipt.pdf', name: 'receipt.pdf', size: 5, type: 'application/pdf' }, +} + +function jsonResponse(body: unknown, status = 200) { + return { + ok: status >= 200 && status < 300, + status, + text: async () => JSON.stringify(body), + json: async () => body, + } +} + +beforeEach(() => { + vi.clearAllMocks() + vi.stubGlobal('fetch', mockFetch) + hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({ + success: true, + userId: 'user-1', + authType: 'internal_jwt', + }) + mockProcessFilesToUserFiles.mockReturnValue([ + { key: 'uploads/receipt.pdf', name: 'receipt.pdf', size: 5, type: 'application/pdf' }, + ]) + mockAssertToolFileAccess.mockResolvedValue(null) + mockDownloadFileFromStorage.mockResolvedValue(Buffer.from('receipt-bytes')) +}) + +describe('POST /api/tools/brex/upload-receipt', () => { + it('rejects unauthenticated requests', async () => { + hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValueOnce({ + success: false, + error: 'unauthorized', + }) + + const response = await POST(createMockRequest('POST', baseBody)) + expect(response.status).toBe(401) + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('creates a receipt upload for an expense and PUTs the file to the pre-signed URL', async () => { + mockFetch + .mockResolvedValueOnce( + jsonResponse({ id: 'receipt_1', uri: 'https://s3.example.com/presigned' }) + ) + .mockResolvedValueOnce(jsonResponse({})) + + const response = await POST(createMockRequest('POST', baseBody)) + expect(response.status).toBe(200) + const data = await response.json() + expect(data).toEqual({ + success: true, + output: { receiptId: 'receipt_1', receiptName: 'receipt.pdf', expenseId: 'expense_123' }, + }) + + expect(mockFetch).toHaveBeenCalledTimes(2) + const [createUrl, createInit] = mockFetch.mock.calls[0] + expect(createUrl).toBe('https://api.brex.com/v1/expenses/card/expense_123/receipt_upload') + expect(createInit.method).toBe('POST') + expect(createInit.headers.Authorization).toBe('Bearer bxt_test_token') + expect(JSON.parse(createInit.body)).toEqual({ receipt_name: 'receipt.pdf' }) + + const [uploadUrl, uploadInit] = mockFetch.mock.calls[1] + expect(uploadUrl).toBe('https://s3.example.com/presigned') + expect(uploadInit.method).toBe('PUT') + }) + + it('uses receipt match when no expense ID is provided', async () => { + mockFetch + .mockResolvedValueOnce( + jsonResponse({ id: 'receipt_2', uri: 'https://s3.example.com/presigned' }) + ) + .mockResolvedValueOnce(jsonResponse({})) + + const response = await POST( + createMockRequest('POST', { apiKey: 'bxt_test_token', file: baseBody.file }) + ) + expect(response.status).toBe(200) + const data = await response.json() + expect(data.output).toEqual({ + receiptId: 'receipt_2', + receiptName: 'receipt.pdf', + expenseId: null, + }) + + const [createUrl] = mockFetch.mock.calls[0] + expect(createUrl).toBe('https://api.brex.com/v1/expenses/card/receipt_match') + }) + + it('honors a receipt name override', async () => { + mockFetch + .mockResolvedValueOnce( + jsonResponse({ id: 'receipt_3', uri: 'https://s3.example.com/presigned' }) + ) + .mockResolvedValueOnce(jsonResponse({})) + + const response = await POST( + createMockRequest('POST', { ...baseBody, receiptName: 'march-dinner.pdf' }) + ) + expect(response.status).toBe(200) + const [, createInit] = mockFetch.mock.calls[0] + expect(JSON.parse(createInit.body)).toEqual({ receipt_name: 'march-dinner.pdf' }) + }) + + it('propagates Brex API errors', async () => { + mockFetch.mockResolvedValueOnce(jsonResponse({ message: 'Expense not found' }, 404)) + + const response = await POST(createMockRequest('POST', baseBody)) + expect(response.status).toBe(404) + const data = await response.json() + expect(data.success).toBe(false) + expect(data.error).toContain('Expense not found') + expect(mockFetch).toHaveBeenCalledTimes(1) + }) + + it('rejects files over the 50 MB limit', async () => { + mockDownloadFileFromStorage.mockResolvedValueOnce(Buffer.alloc(50 * 1024 * 1024 + 1)) + + const response = await POST(createMockRequest('POST', baseBody)) + expect(response.status).toBe(400) + const data = await response.json() + expect(data.error).toContain('50 MB') + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('fails when the pre-signed upload fails', async () => { + mockFetch + .mockResolvedValueOnce( + jsonResponse({ id: 'receipt_4', uri: 'https://s3.example.com/presigned' }) + ) + .mockResolvedValueOnce(jsonResponse({}, 403)) + + const response = await POST(createMockRequest('POST', baseBody)) + expect(response.status).toBe(502) + const data = await response.json() + expect(data.success).toBe(false) + }) + + it('denies access to files the caller cannot read', async () => { + const deniedResponse = new Response( + JSON.stringify({ success: false, error: 'File not found' }), + { + status: 404, + } + ) + mockAssertToolFileAccess.mockResolvedValueOnce(deniedResponse) + + const response = await POST(createMockRequest('POST', baseBody)) + expect(response.status).toBe(404) + expect(mockFetch).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/app/api/tools/brex/upload-receipt/route.ts b/apps/sim/app/api/tools/brex/upload-receipt/route.ts new file mode 100644 index 0000000000..f8a9f8e6c5 --- /dev/null +++ b/apps/sim/app/api/tools/brex/upload-receipt/route.ts @@ -0,0 +1,129 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { brexUploadReceiptContract } from '@/lib/api/contracts/tools/brex' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils' +import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' +import { BREX_API_BASE, buildBrexHeaders } from '@/tools/brex/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('BrexUploadReceiptAPI') + +const MAX_RECEIPT_SIZE_BYTES = 50 * 1024 * 1024 + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized Brex receipt upload attempt: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + const parsed = await parseRequest(brexUploadReceiptContract, request, {}) + if (!parsed.success) return parsed.response + const { apiKey, expenseId, file, receiptName } = parsed.data.body + + const userFiles = processFilesToUserFiles([file as RawFileInput], requestId, logger) + if (userFiles.length === 0) { + return NextResponse.json({ success: false, error: 'Invalid file input' }, { status: 400 }) + } + + const userFile = userFiles[0] + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied + + const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) + if (fileBuffer.length > MAX_RECEIPT_SIZE_BYTES) { + return NextResponse.json( + { success: false, error: 'Receipt file exceeds the 50 MB limit' }, + { status: 400 } + ) + } + + const effectiveReceiptName = receiptName?.trim() || userFile.name + const trimmedExpenseId = expenseId?.trim() || undefined + const endpoint = trimmedExpenseId + ? `${BREX_API_BASE}/v1/expenses/card/${encodeURIComponent(trimmedExpenseId)}/receipt_upload` + : `${BREX_API_BASE}/v1/expenses/card/receipt_match` + + logger.info( + `[${requestId}] Creating Brex ${trimmedExpenseId ? 'receipt upload' : 'receipt match'}: ${effectiveReceiptName} (${fileBuffer.length} bytes)` + ) + + const createResponse = await fetch(endpoint, { + method: 'POST', + headers: buildBrexHeaders(apiKey), + body: JSON.stringify({ receipt_name: effectiveReceiptName }), + }) + + if (!createResponse.ok) { + const errorText = await createResponse.text() + logger.error(`[${requestId}] Brex API error:`, { + status: createResponse.status, + error: errorText, + }) + let message = errorText + try { + message = JSON.parse(errorText).message ?? errorText + } catch { + message = errorText + } + return NextResponse.json( + { success: false, error: `Brex API error (${createResponse.status}): ${message}` }, + { status: createResponse.status } + ) + } + + const createData = await createResponse.json() + if (!createData.uri || !createData.id) { + return NextResponse.json( + { success: false, error: 'Brex did not return an upload URL' }, + { status: 502 } + ) + } + + const uploadResponse = await fetch(createData.uri, { + method: 'PUT', + body: new Uint8Array(fileBuffer), + }) + + if (!uploadResponse.ok) { + logger.error(`[${requestId}] Receipt upload to pre-signed URL failed:`, { + status: uploadResponse.status, + }) + return NextResponse.json( + { success: false, error: `Failed to upload receipt file (${uploadResponse.status})` }, + { status: 502 } + ) + } + + logger.info(`[${requestId}] Receipt uploaded successfully (ID: ${createData.id})`) + + return NextResponse.json({ + success: true, + output: { + receiptId: createData.id, + receiptName: effectiveReceiptName, + expenseId: trimmedExpenseId ?? null, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Unexpected error:`, error) + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Unknown error') }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/blocks/blocks/brex.ts b/apps/sim/blocks/blocks/brex.ts new file mode 100644 index 0000000000..43efd6983f --- /dev/null +++ b/apps/sim/blocks/blocks/brex.ts @@ -0,0 +1,671 @@ +import { BrexIcon } from '@/components/icons' +import type { BlockConfig, BlockMeta } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import { normalizeFileInput } from '@/blocks/utils' +import type { BrexResponse } from '@/tools/brex/types' + +export const BrexBlock: BlockConfig = { + type: 'brex', + name: 'Brex', + description: 'Manage expenses, receipts, transactions, and team data in Brex', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrates Brex into the workflow. List and update expenses, upload and match receipts, view card and cash transactions, accounts, budgets, spend limits, vendors, transfers, and team data.', + docsLink: 'https://docs.sim.ai/integrations/brex', + category: 'tools', + integrationType: IntegrationType.Commerce, + bgColor: '#171717', + icon: BrexIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // Expenses + { label: 'List Expenses', id: 'list_expenses' }, + { label: 'Get Expense', id: 'get_expense' }, + { label: 'Update Expense Memo', id: 'update_expense' }, + { label: 'Upload Receipt', id: 'upload_receipt' }, + { label: 'Match Receipt', id: 'match_receipt' }, + // Transactions & accounts + { label: 'List Card Transactions', id: 'list_card_transactions' }, + { label: 'List Cash Transactions', id: 'list_cash_transactions' }, + { label: 'List Card Accounts', id: 'list_card_accounts' }, + { label: 'List Cash Accounts', id: 'list_cash_accounts' }, + { label: 'Get Cash Account', id: 'get_cash_account' }, + { label: 'List Card Statements', id: 'list_card_statements' }, + { label: 'List Cash Statements', id: 'list_cash_statements' }, + // Team + { label: 'List Users', id: 'list_users' }, + { label: 'Get User', id: 'get_user' }, + { label: 'Get Current User', id: 'get_current_user' }, + { label: 'List Departments', id: 'list_departments' }, + { label: 'List Locations', id: 'list_locations' }, + { label: 'List Titles', id: 'list_titles' }, + { label: 'List Cards', id: 'list_cards' }, + { label: 'Get Company', id: 'get_company' }, + // Budgets + { label: 'List Budgets', id: 'list_budgets' }, + { label: 'Get Budget', id: 'get_budget' }, + { label: 'List Spend Limits', id: 'list_spend_limits' }, + { label: 'Get Spend Limit', id: 'get_spend_limit' }, + // Payments + { label: 'List Vendors', id: 'list_vendors' }, + { label: 'Get Vendor', id: 'get_vendor' }, + { label: 'List Transfers', id: 'list_transfers' }, + { label: 'Get Transfer', id: 'get_transfer' }, + ], + value: () => 'list_expenses', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + password: true, + placeholder: 'Enter your Brex user token', + required: true, + }, + { + id: 'expenseId', + title: 'Expense ID', + type: 'short-input', + placeholder: 'ID of the expense', + condition: { + field: 'operation', + value: ['get_expense', 'update_expense', 'upload_receipt'], + }, + required: { + field: 'operation', + value: ['get_expense', 'update_expense', 'upload_receipt'], + }, + }, + { + id: 'memo', + title: 'Memo', + type: 'long-input', + placeholder: 'New memo for the expense', + condition: { field: 'operation', value: 'update_expense' }, + required: { field: 'operation', value: 'update_expense' }, + }, + { + id: 'uploadReceiptFile', + title: 'Receipt File', + type: 'file-upload', + canonicalParamId: 'file', + placeholder: 'Upload receipt file', + mode: 'basic', + multiple: false, + condition: { field: 'operation', value: ['upload_receipt', 'match_receipt'] }, + required: { field: 'operation', value: ['upload_receipt', 'match_receipt'] }, + }, + { + id: 'receiptFileReference', + title: 'Receipt File', + type: 'short-input', + canonicalParamId: 'file', + placeholder: 'Reference a file from a previous block', + mode: 'advanced', + condition: { field: 'operation', value: ['upload_receipt', 'match_receipt'] }, + required: { field: 'operation', value: ['upload_receipt', 'match_receipt'] }, + }, + { + id: 'receiptName', + title: 'Receipt Name', + type: 'short-input', + placeholder: 'Receipt file name with extension (defaults to the uploaded file name)', + mode: 'advanced', + condition: { field: 'operation', value: ['upload_receipt', 'match_receipt'] }, + }, + { + id: 'accountId', + title: 'Cash Account ID', + type: 'short-input', + placeholder: 'ID of the cash account (Get Cash Account defaults to primary)', + condition: { + field: 'operation', + value: ['list_cash_transactions', 'list_cash_statements', 'get_cash_account'], + }, + required: { + field: 'operation', + value: ['list_cash_transactions', 'list_cash_statements'], + }, + }, + { + id: 'userId', + title: 'User ID', + type: 'short-input', + placeholder: 'ID of the user (optional filter for List Cards)', + condition: { field: 'operation', value: ['get_user', 'list_cards'] }, + required: { field: 'operation', value: 'get_user' }, + }, + { + id: 'budgetId', + title: 'Budget ID', + type: 'short-input', + placeholder: 'ID of the budget', + condition: { field: 'operation', value: 'get_budget' }, + required: { field: 'operation', value: 'get_budget' }, + }, + { + id: 'spendLimitId', + title: 'Spend Limit ID', + type: 'short-input', + placeholder: 'ID of the spend limit', + condition: { field: 'operation', value: 'get_spend_limit' }, + required: { field: 'operation', value: 'get_spend_limit' }, + }, + { + id: 'vendorId', + title: 'Vendor ID', + type: 'short-input', + placeholder: 'ID of the vendor', + condition: { field: 'operation', value: 'get_vendor' }, + required: { field: 'operation', value: 'get_vendor' }, + }, + { + id: 'transferId', + title: 'Transfer ID', + type: 'short-input', + placeholder: 'ID of the transfer', + condition: { field: 'operation', value: 'get_transfer' }, + required: { field: 'operation', value: 'get_transfer' }, + }, + { + id: 'email', + title: 'Email', + type: 'short-input', + placeholder: 'Filter users by exact email address', + mode: 'advanced', + condition: { field: 'operation', value: 'list_users' }, + }, + { + id: 'name', + title: 'Name Filter', + type: 'short-input', + placeholder: 'Filter results by name', + mode: 'advanced', + condition: { + field: 'operation', + value: ['list_departments', 'list_locations', 'list_titles', 'list_vendors'], + }, + }, + { + id: 'userIds', + title: 'User IDs', + type: 'short-input', + placeholder: 'Comma-separated user IDs to filter by', + mode: 'advanced', + condition: { field: 'operation', value: ['list_expenses', 'list_card_transactions'] }, + }, + { + id: 'statuses', + title: 'Expense Statuses', + type: 'short-input', + placeholder: 'e.g., APPROVED, SETTLED (comma-separated)', + mode: 'advanced', + condition: { field: 'operation', value: 'list_expenses' }, + }, + { + id: 'paymentStatuses', + title: 'Payment Statuses', + type: 'short-input', + placeholder: 'e.g., CLEARED, REFUNDED (comma-separated)', + mode: 'advanced', + condition: { field: 'operation', value: 'list_expenses' }, + }, + { + id: 'purchasedAtStart', + title: 'Purchased After', + type: 'short-input', + placeholder: 'ISO 8601 timestamp (e.g., 2026-01-01T00:00:00Z)', + mode: 'advanced', + condition: { field: 'operation', value: 'list_expenses' }, + wandConfig: { + enabled: true, + generationType: 'timestamp', + prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.', + placeholder: 'Describe the start date (e.g., "beginning of last month")...', + }, + }, + { + id: 'purchasedAtEnd', + title: 'Purchased Before', + type: 'short-input', + placeholder: 'ISO 8601 timestamp (e.g., 2026-02-01T00:00:00Z)', + mode: 'advanced', + condition: { field: 'operation', value: 'list_expenses' }, + wandConfig: { + enabled: true, + generationType: 'timestamp', + prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.', + placeholder: 'Describe the end date (e.g., "end of last month")...', + }, + }, + { + id: 'postedAtStart', + title: 'Posted After', + type: 'short-input', + placeholder: 'ISO 8601 timestamp (e.g., 2026-01-01T00:00:00Z)', + mode: 'advanced', + condition: { + field: 'operation', + value: ['list_card_transactions', 'list_cash_transactions'], + }, + wandConfig: { + enabled: true, + generationType: 'timestamp', + prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.', + placeholder: 'Describe the start date (e.g., "last Monday")...', + }, + }, + { + id: 'memberUserIds', + title: 'Member User IDs', + type: 'short-input', + placeholder: 'Comma-separated user IDs to filter spend limits by member', + mode: 'advanced', + condition: { field: 'operation', value: 'list_spend_limits' }, + }, + { + id: 'cursor', + title: 'Cursor', + type: 'short-input', + placeholder: 'Pagination cursor from a previous response', + mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'list_expenses', + 'list_card_transactions', + 'list_cash_transactions', + 'list_cash_accounts', + 'list_card_statements', + 'list_cash_statements', + 'list_users', + 'list_departments', + 'list_locations', + 'list_titles', + 'list_cards', + 'list_budgets', + 'list_spend_limits', + 'list_vendors', + 'list_transfers', + ], + }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Number of results to return (default 100, max 1000)', + mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'list_expenses', + 'list_card_transactions', + 'list_cash_transactions', + 'list_cash_accounts', + 'list_card_statements', + 'list_cash_statements', + 'list_users', + 'list_departments', + 'list_locations', + 'list_titles', + 'list_cards', + 'list_budgets', + 'list_spend_limits', + 'list_vendors', + 'list_transfers', + ], + }, + }, + ], + tools: { + access: [ + 'brex_list_expenses', + 'brex_get_expense', + 'brex_update_expense', + 'brex_upload_receipt', + 'brex_match_receipt', + 'brex_list_card_transactions', + 'brex_list_cash_transactions', + 'brex_list_card_accounts', + 'brex_list_cash_accounts', + 'brex_get_cash_account', + 'brex_list_card_statements', + 'brex_list_cash_statements', + 'brex_list_users', + 'brex_get_user', + 'brex_get_current_user', + 'brex_list_departments', + 'brex_list_locations', + 'brex_list_titles', + 'brex_list_cards', + 'brex_get_company', + 'brex_list_budgets', + 'brex_get_budget', + 'brex_list_spend_limits', + 'brex_get_spend_limit', + 'brex_list_vendors', + 'brex_get_vendor', + 'brex_list_transfers', + 'brex_get_transfer', + ], + config: { + tool: (params) => `brex_${params.operation}`, + params: (params) => { + const { operation, apiKey } = params + const result: Record = { apiKey } + + switch (operation) { + case 'list_expenses': + if (params.userIds) result.userIds = params.userIds + if (params.statuses) result.statuses = params.statuses + if (params.paymentStatuses) result.paymentStatuses = params.paymentStatuses + if (params.purchasedAtStart) result.purchasedAtStart = params.purchasedAtStart + if (params.purchasedAtEnd) result.purchasedAtEnd = params.purchasedAtEnd + break + case 'get_expense': + result.expenseId = params.expenseId + break + case 'update_expense': + result.expenseId = params.expenseId + result.memo = params.memo + break + case 'upload_receipt': + case 'match_receipt': { + const file = normalizeFileInput(params.file, { single: true }) + if (file) result.file = file + if (operation === 'upload_receipt') result.expenseId = params.expenseId + if (params.receiptName) result.receiptName = params.receiptName + break + } + case 'list_card_transactions': + if (params.userIds) result.userIds = params.userIds + if (params.postedAtStart) result.postedAtStart = params.postedAtStart + break + case 'list_cash_transactions': + result.accountId = params.accountId + if (params.postedAtStart) result.postedAtStart = params.postedAtStart + break + case 'list_cash_statements': + result.accountId = params.accountId + break + case 'get_cash_account': + if (params.accountId) result.accountId = params.accountId + break + case 'list_users': + if (params.email) result.email = params.email + break + case 'get_user': + result.userId = params.userId + break + case 'list_cards': + if (params.userId) result.userId = params.userId + break + case 'list_departments': + case 'list_locations': + case 'list_titles': + case 'list_vendors': + if (params.name) result.name = params.name + break + case 'list_spend_limits': + if (params.memberUserIds) result.memberUserIds = params.memberUserIds + break + case 'get_budget': + result.budgetId = params.budgetId + break + case 'get_spend_limit': + result.spendLimitId = params.spendLimitId + break + case 'get_vendor': + result.vendorId = params.vendorId + break + case 'get_transfer': + result.transferId = params.transferId + break + default: + break + } + + if (params.cursor) result.cursor = String(params.cursor) + if (params.limit) result.limit = String(params.limit) + + return result + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Brex user token' }, + expenseId: { type: 'string', description: 'Expense ID' }, + memo: { type: 'string', description: 'New memo for the expense' }, + file: { type: 'json', description: 'Receipt file to upload (canonical param)' }, + receiptName: { type: 'string', description: 'Receipt file name including extension' }, + accountId: { type: 'string', description: 'Cash account ID' }, + userId: { type: 'string', description: 'User ID' }, + budgetId: { type: 'string', description: 'Budget ID' }, + spendLimitId: { type: 'string', description: 'Spend limit ID' }, + vendorId: { type: 'string', description: 'Vendor ID' }, + transferId: { type: 'string', description: 'Transfer ID' }, + email: { type: 'string', description: 'Email filter for listing users' }, + name: { type: 'string', description: 'Name filter for departments, locations, or vendors' }, + userIds: { type: 'string', description: 'Comma-separated user IDs filter' }, + statuses: { type: 'string', description: 'Comma-separated expense statuses filter' }, + paymentStatuses: { type: 'string', description: 'Comma-separated payment statuses filter' }, + purchasedAtStart: { type: 'string', description: 'Purchased-after ISO 8601 timestamp filter' }, + purchasedAtEnd: { type: 'string', description: 'Purchased-before ISO 8601 timestamp filter' }, + postedAtStart: { type: 'string', description: 'Posted-after ISO 8601 timestamp filter' }, + memberUserIds: { + type: 'string', + description: 'Comma-separated member user IDs filter for spend limits', + }, + cursor: { type: 'string', description: 'Pagination cursor' }, + limit: { type: 'string', description: 'Number of results to return' }, + }, + outputs: { + items: { type: 'json', description: 'Items returned by list operations' }, + nextCursor: { type: 'string', description: 'Cursor for fetching the next page of results' }, + accounts: { type: 'json', description: 'Card accounts returned by List Card Accounts' }, + id: { type: 'string', description: 'ID of the fetched or updated resource' }, + memo: { type: 'string', description: 'Memo on the expense' }, + status: { type: 'string', description: 'Status of the expense or user' }, + paymentStatus: { type: 'string', description: 'Payment status of the expense' }, + expenseType: { type: 'string', description: 'Type of the expense' }, + category: { type: 'string', description: 'Merchant category of the expense' }, + merchantId: { type: 'string', description: 'Merchant ID' }, + merchant: { type: 'json', description: 'Merchant details' }, + budgetId: { type: 'string', description: 'Budget ID' }, + budget: { type: 'json', description: 'Budget details' }, + departmentId: { type: 'string', description: 'Department ID' }, + department: { type: 'json', description: 'Department details' }, + locationId: { type: 'string', description: 'Location ID' }, + location: { type: 'json', description: 'Location details' }, + userId: { type: 'string', description: 'User ID associated with the expense' }, + user: { type: 'json', description: 'User details' }, + originalAmount: { type: 'json', description: 'Original transaction amount' }, + billingAmount: { type: 'json', description: 'Amount billed to the account' }, + purchasedAmount: { type: 'json', description: 'Amount at the time of purchase' }, + usdEquivalentAmount: { type: 'json', description: 'USD equivalent amount' }, + purchasedAt: { type: 'string', description: 'Purchase timestamp' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, + paymentPostedAt: { type: 'string', description: 'Timestamp the payment was posted' }, + receipts: { type: 'json', description: 'Receipts attached to the expense' }, + dashboardUrl: { type: 'string', description: 'Link to the expense in the Brex dashboard' }, + receiptId: { type: 'string', description: 'ID of the uploaded or matched receipt' }, + receiptName: { type: 'string', description: 'Name the receipt was uploaded with' }, + expenseId: { type: 'string', description: 'ID of the expense the receipt was attached to' }, + firstName: { type: 'string', description: 'First name of the user' }, + lastName: { type: 'string', description: 'Last name of the user' }, + email: { type: 'string', description: 'Email address of the user' }, + managerId: { type: 'string', description: 'Manager ID of the user' }, + titleId: { type: 'string', description: 'Title ID of the user' }, + legalName: { type: 'string', description: 'Legal name of the company' }, + mailingAddress: { type: 'json', description: 'Mailing address of the company' }, + accountType: { type: 'string', description: 'Brex account type of the company' }, + name: { type: 'string', description: 'Name of the account, budget, or spend limit' }, + currentBalance: { type: 'json', description: 'Current balance of the cash account' }, + availableBalance: { type: 'json', description: 'Available balance of the cash account' }, + accountNumber: { type: 'string', description: 'Bank account number of the cash account' }, + routingNumber: { type: 'string', description: 'Bank routing number of the cash account' }, + primary: { type: 'boolean', description: 'Whether the cash account is primary' }, + accountId: { type: 'string', description: 'Account ID of the budget or spend limit' }, + description: { type: 'string', description: 'Description of the budget or spend limit' }, + parentBudgetId: { type: 'string', description: 'Parent budget ID' }, + ownerUserIds: { type: 'json', description: 'Owner user IDs of the budget or spend limit' }, + memberUserIds: { type: 'json', description: 'Member user IDs of the spend limit' }, + periodRecurrenceType: { type: 'string', description: 'Period recurrence type' }, + spendType: { type: 'string', description: 'Spend type of the spend limit' }, + startDate: { type: 'string', description: 'Start date of the budget or spend limit' }, + endDate: { type: 'string', description: 'End date of the budget or spend limit' }, + amount: { type: 'json', description: 'Amount of the budget' }, + spendBudgetStatus: { type: 'string', description: 'Status of the budget' }, + limitType: { type: 'string', description: 'Limit type of the budget' }, + currentPeriodBalance: { + type: 'json', + description: 'Current period balance of the spend limit', + }, + authorizationSettings: { + type: 'json', + description: 'Authorization settings of the spend limit', + }, + companyName: { type: 'string', description: 'Company name of the vendor' }, + phone: { type: 'string', description: 'Phone number of the vendor' }, + paymentAccounts: { type: 'json', description: 'Payment accounts of the vendor' }, + counterparty: { type: 'json', description: 'Counterparty of the transfer' }, + paymentType: { type: 'string', description: 'Payment type of the transfer' }, + processDate: { type: 'string', description: 'Process date of the transfer' }, + originatingAccount: { type: 'json', description: 'Originating account of the transfer' }, + cancellationReason: { type: 'string', description: 'Cancellation reason of the transfer' }, + estimatedDeliveryDate: { + type: 'string', + description: 'Estimated delivery date of the transfer', + }, + creatorUserId: { type: 'string', description: 'ID of the user who created the transfer' }, + createdAt: { type: 'string', description: 'Creation timestamp of the transfer' }, + displayName: { type: 'string', description: 'Display name of the transfer' }, + externalMemo: { type: 'string', description: 'External memo of the transfer' }, + }, +} + +export const BrexBlockMeta = { + tags: ['payments'], + templates: [ + { + icon: BrexIcon, + title: 'Brex receipt auto-attach', + prompt: + 'Build a workflow that takes an uploaded receipt file and sends it to Brex with the Match Receipt operation so Brex automatically pairs it with the right card expense.', + modules: ['workflows', 'files'], + category: 'operations', + tags: ['automation'], + }, + { + icon: BrexIcon, + title: 'Brex daily expense digest', + prompt: + 'Build a scheduled workflow that runs every weekday morning, lists Brex expenses settled in the last 24 hours, summarizes total spend by merchant category, and posts the digest to a Slack channel.', + modules: ['workflows', 'scheduled'], + category: 'operations', + tags: ['automation'], + alsoIntegrations: ['slack'], + }, + { + icon: BrexIcon, + title: 'Brex memo enforcer', + prompt: + 'Build a scheduled workflow that lists approved Brex expenses, finds ones missing a memo, has an agent draft a memo from the merchant and amount details, and updates each expense with the drafted memo.', + modules: ['agent', 'workflows', 'scheduled'], + category: 'operations', + tags: ['automation'], + }, + { + icon: BrexIcon, + title: 'Brex spend anomaly alert', + prompt: + 'Build a scheduled workflow that lists recent Brex card transactions, flags any transaction above a configurable threshold, and emails the finance team a report of flagged transactions with merchant details.', + modules: ['workflows', 'scheduled'], + category: 'operations', + tags: ['automation'], + alsoIntegrations: ['gmail'], + }, + { + icon: BrexIcon, + title: 'Brex cash balance monitor', + prompt: + 'Build a scheduled workflow that checks Brex cash account balances every morning and sends a Slack alert when the available balance of any account drops below a set threshold.', + modules: ['workflows', 'scheduled'], + category: 'operations', + tags: ['automation'], + alsoIntegrations: ['slack'], + }, + { + icon: BrexIcon, + title: 'Brex budget utilization report', + prompt: + 'Build a weekly workflow that lists Brex budgets and spend limits, computes utilization for each budget from its amount and current period balance, stores the results in a table, and emails a summary report.', + modules: ['workflows', 'scheduled', 'tables'], + category: 'operations', + tags: ['automation'], + }, + { + icon: BrexIcon, + title: 'Brex team directory assistant', + prompt: + 'Build an agent that answers questions about company spend and team structure by looking up Brex users, departments, locations, and their expenses on demand.', + modules: ['agent'], + category: 'productivity', + tags: ['automation'], + }, + { + icon: BrexIcon, + title: 'Brex vendor payment tracker', + prompt: + 'Build a workflow that lists Brex vendors and recent transfers, reconciles transfer statuses against expected payments stored in a table, and flags any failed or delayed payments.', + modules: ['workflows', 'tables'], + category: 'operations', + tags: ['automation'], + }, + ], + skills: [ + { + name: 'spend-report', + description: + 'Summarize Brex spend over a period, broken down by category, merchant, and user.', + content: + '# Spend Report\n\nBuild a clear summary of company spend from Brex expenses.\n\n## Steps\n1. List expenses filtered to the requested period using the purchased-at date filters.\n2. Group expenses by merchant category, merchant, and user, totaling billing amounts (amounts are in cents).\n3. Highlight the largest expenses and any with OUT_OF_POLICY status.\n\n## Output\nReturn total spend, a breakdown by category and merchant, the top spenders, and any flagged out-of-policy expenses with dashboard links.', + }, + { + name: 'attach-receipt', + description: 'Upload a receipt file and attach it to the right Brex expense.', + content: + '# Attach a Receipt\n\nGet a receipt onto the correct Brex expense.\n\n## Steps\n1. If the target expense is known, use Upload Receipt with the expense ID.\n2. If not, use Match Receipt so Brex pairs the receipt with the right expense automatically.\n3. Confirm the upload succeeded and capture the receipt ID.\n\n## Output\nReturn the receipt ID, the receipt name, and the expense it was attached to (or note that Brex is matching it automatically).', + }, + { + name: 'memo-cleanup', + description: 'Find Brex expenses missing memos and fill them in from merchant details.', + content: + '# Memo Cleanup\n\nKeep expense memos complete for accounting.\n\n## Steps\n1. List recent expenses and find ones with an empty memo.\n2. For each, draft a short memo from the merchant descriptor, category, and amount.\n3. Update each expense with the drafted memo using Update Expense Memo.\n\n## Output\nReturn the list of expenses updated, each with its new memo, and any expenses that could not be updated.', + }, + { + name: 'budget-utilization', + description: 'Report utilization for Brex budgets and spend limits.', + content: + '# Budget Utilization\n\nShow how much of each budget and spend limit has been used.\n\n## Steps\n1. List budgets and capture each amount and status.\n2. List spend limits and capture each current period balance.\n3. Compute utilization where both an amount and a balance are available (amounts are in cents).\n\n## Output\nReturn each budget and spend limit with its owner, period, amount, and utilization, flagging any that are near or over their limit.', + }, + { + name: 'cash-balance-check', + description: 'Check Brex cash account balances and recent account activity.', + content: + '# Cash Balance Check\n\nGive a quick read on company cash in Brex.\n\n## Steps\n1. List cash accounts and capture current and available balances (amounts are in cents).\n2. For the primary account, list recent cash transactions.\n3. Note any unusually large recent movements.\n\n## Output\nReturn each account with its balances, the most recent transactions for the primary account, and any large movements worth a look.', + }, + { + name: 'statement-reconciliation', + description: 'Reconcile a Brex card statement period against its settled transactions.', + content: + '# Statement Reconciliation\n\nTie a card statement back to its underlying transactions.\n\n## Steps\n1. List card statements and pick the period to reconcile.\n2. List card transactions posted within that period using the posted-at filter.\n3. Compare transaction totals to the statement start and end balances and flag gaps.\n\n## Output\nReturn the statement period, its balances, the transaction total for the period, and any discrepancy that needs review.', + }, + ], +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index bc7d8b42bb..86fd6ececb 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -22,6 +22,7 @@ import { AttioBlock, AttioBlockMeta } from '@/blocks/blocks/attio' import { AzureDevOpsBlock, AzureDevOpsBlockMeta } from '@/blocks/blocks/azure_devops' import { BoxBlock, BoxBlockMeta } from '@/blocks/blocks/box' import { BrandfetchBlock, BrandfetchBlockMeta } from '@/blocks/blocks/brandfetch' +import { BrexBlock, BrexBlockMeta } from '@/blocks/blocks/brex' import { BrightDataBlock, BrightDataBlockMeta } from '@/blocks/blocks/brightdata' import { BrowserUseBlock, BrowserUseBlockMeta } from '@/blocks/blocks/browser_use' import { CalComBlock, CalComBlockMeta } from '@/blocks/blocks/calcom' @@ -345,6 +346,7 @@ const BLOCK_REGISTRY: Record = { azure_devops: AzureDevOpsBlock, box: BoxBlock, brandfetch: BrandfetchBlock, + brex: BrexBlock, brightdata: BrightDataBlock, browser_use: BrowserUseBlock, calcom: CalComBlock, @@ -641,6 +643,7 @@ const BLOCK_META_REGISTRY: Record = { azure_devops: AzureDevOpsBlockMeta, box: BoxBlockMeta, brandfetch: BrandfetchBlockMeta, + brex: BrexBlockMeta, brightdata: BrightDataBlockMeta, browser_use: BrowserUseBlockMeta, calcom: CalComBlockMeta, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 6161069bef..90987f14fd 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2261,6 +2261,17 @@ export function BrandfetchIcon(props: SVGProps) { ) } +export function BrexIcon(props: SVGProps) { + return ( + + + + ) +} + export function BrightDataIcon(props: SVGProps) { return ( +export type BrexUploadReceiptRouteResponse = ContractJsonResponse diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index 7344cb8ee1..34f8cec180 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -24,6 +24,7 @@ import { BoxCompanyIcon, BrainIcon, BrandfetchIcon, + BrexIcon, BrightDataIcon, BrowserUseIcon, CalComIcon, @@ -241,6 +242,7 @@ export const blockTypeToIconMap: Record = { azure_devops: AzureIcon, box: BoxCompanyIcon, brandfetch: BrandfetchIcon, + brex: BrexIcon, brightdata: BrightDataIcon, browser_use: BrowserUseIcon, calcom: CalComIcon, diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 72301855dd..7ea104cafd 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -2167,6 +2167,137 @@ "integrationType": "sales", "tags": ["enrichment", "marketing"] }, + { + "type": "brex", + "slug": "brex", + "name": "Brex", + "description": "Manage expenses, receipts, transactions, and team data in Brex", + "longDescription": "Integrates Brex into the workflow. List and update expenses, upload and match receipts, view card and cash transactions, accounts, budgets, spend limits, vendors, transfers, and team data.", + "bgColor": "#171717", + "iconName": "BrexIcon", + "docsUrl": "https://docs.sim.ai/integrations/brex", + "operations": [ + { + "name": "List Expenses", + "description": "List expenses in the Brex account with optional filters for user, status, payment status, and purchase date range" + }, + { + "name": "Get Expense", + "description": "Get a single Brex expense by its ID, including merchant, user, and receipt details" + }, + { + "name": "Update Expense Memo", + "description": "Update the memo of a Brex card expense" + }, + { + "name": "Upload Receipt", + "description": "Upload a receipt file and attach it to a specific Brex card expense" + }, + { + "name": "Match Receipt", + "description": "Upload a receipt file and let Brex automatically match it with existing expenses" + }, + { + "name": "List Card Transactions", + "description": "List settled card transactions for all Brex card accounts" + }, + { + "name": "List Cash Transactions", + "description": "List transactions for a Brex cash account" + }, + { + "name": "List Card Accounts", + "description": "List all Brex card accounts with balances and limits" + }, + { + "name": "List Cash Accounts", + "description": "List all Brex cash accounts with balances and account details" + }, + { + "name": "Get Cash Account", + "description": "Get a Brex cash account by ID, or the primary cash account when no ID is provided" + }, + { + "name": "List Card Statements", + "description": "List finalized statements for the primary Brex card account" + }, + { + "name": "List Cash Statements", + "description": "List finalized statements for a Brex cash account" + }, + { + "name": "List Users", + "description": "List users in the Brex account, optionally filtered by email" + }, + { + "name": "Get User", + "description": "Get a Brex user by their ID" + }, + { + "name": "Get Current User", + "description": "Get the Brex user associated with the API token" + }, + { + "name": "List Departments", + "description": "List departments in the Brex account, optionally filtered by name" + }, + { + "name": "List Locations", + "description": "List locations in the Brex account, optionally filtered by name" + }, + { + "name": "List Titles", + "description": "List job titles in the Brex account, optionally filtered by name" + }, + { + "name": "List Cards", + "description": "List cards in the Brex account, optionally filtered by card owner" + }, + { + "name": "Get Company", + "description": "Get the Brex company associated with the API token" + }, + { + "name": "List Budgets", + "description": "List budgets in the Brex account" + }, + { + "name": "Get Budget", + "description": "Get a Brex budget by its ID" + }, + { + "name": "List Spend Limits", + "description": "List spend limits in the Brex account, optionally filtered by member user" + }, + { + "name": "Get Spend Limit", + "description": "Get a Brex spend limit by its ID" + }, + { + "name": "List Vendors", + "description": "List vendors in the Brex account, optionally filtered by name" + }, + { + "name": "Get Vendor", + "description": "Get a Brex vendor by its ID" + }, + { + "name": "List Transfers", + "description": "List money transfers in the Brex account" + }, + { + "name": "Get Transfer", + "description": "Get a Brex money transfer by its ID" + } + ], + "operationCount": 28, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "commerce", + "tags": ["payments"] + }, { "type": "brightdata", "slug": "bright-data", diff --git a/apps/sim/tools/brex/get_budget.ts b/apps/sim/tools/brex/get_budget.ts new file mode 100644 index 0000000000..9496eac14c --- /dev/null +++ b/apps/sim/tools/brex/get_budget.ts @@ -0,0 +1,79 @@ +import type { BrexGetBudgetParams, BrexGetBudgetResponse } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexGetBudgetTool: ToolConfig = { + id: 'brex_get_budget', + name: 'Brex Get Budget', + description: 'Get a Brex budget by its ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + budgetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the budget to fetch', + }, + }, + + request: { + url: (params) => `${BREX_API_BASE}/v2/budgets/${encodeURIComponent(params.budgetId.trim())}`, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + budgetId: data.budget_id ?? '', + accountId: data.account_id ?? '', + name: data.name ?? '', + description: data.description ?? null, + parentBudgetId: data.parent_budget_id ?? null, + ownerUserIds: data.owner_user_ids ?? [], + periodRecurrenceType: data.period_recurrence_type ?? '', + startDate: data.start_date ?? null, + endDate: data.end_date ?? null, + amount: data.amount ?? null, + spendBudgetStatus: data.spend_budget_status ?? '', + limitType: data.limit_type ?? null, + }, + } + }, + + outputs: { + budgetId: { type: 'string', description: 'Unique budget ID' }, + accountId: { type: 'string', description: 'Account ID the budget belongs to' }, + name: { type: 'string', description: 'Budget name' }, + description: { type: 'string', description: 'Budget description', optional: true }, + parentBudgetId: { type: 'string', description: 'Parent budget ID', optional: true }, + ownerUserIds: { type: 'array', description: 'User IDs of the budget owners' }, + periodRecurrenceType: { + type: 'string', + description: 'Budget period recurrence (WEEKLY, MONTHLY, QUARTERLY, YEARLY, ONE_TIME)', + }, + startDate: { type: 'string', description: 'Budget start date', optional: true }, + endDate: { type: 'string', description: 'Budget end date', optional: true }, + amount: { + type: 'json', + description: 'Budget amount', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + spendBudgetStatus: { + type: 'string', + description: 'Budget status (ACTIVE, ARCHIVED, DELETED, EXPIRED)', + }, + limitType: { type: 'string', description: 'Budget limit type (HARD or SOFT)', optional: true }, + }, +} diff --git a/apps/sim/tools/brex/get_cash_account.ts b/apps/sim/tools/brex/get_cash_account.ts new file mode 100644 index 0000000000..f2f913084a --- /dev/null +++ b/apps/sim/tools/brex/get_cash_account.ts @@ -0,0 +1,78 @@ +import type { BrexGetCashAccountParams, BrexGetCashAccountResponse } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexGetCashAccountTool: ToolConfig< + BrexGetCashAccountParams, + BrexGetCashAccountResponse +> = { + id: 'brex_get_cash_account', + name: 'Brex Get Cash Account', + description: 'Get a Brex cash account by ID, or the primary cash account when no ID is provided', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + accountId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ID of the cash account (defaults to the primary cash account)', + }, + }, + + request: { + url: (params) => { + const accountId = params.accountId?.trim() + return accountId + ? `${BREX_API_BASE}/v2/accounts/cash/${encodeURIComponent(accountId)}` + : `${BREX_API_BASE}/v2/accounts/cash/primary` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + name: data.name ?? '', + status: data.status ?? null, + currentBalance: data.current_balance ?? null, + availableBalance: data.available_balance ?? null, + accountNumber: data.account_number ?? '', + routingNumber: data.routing_number ?? '', + primary: data.primary ?? false, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique account ID' }, + name: { type: 'string', description: 'Account name' }, + status: { type: 'string', description: 'Account status', optional: true }, + currentBalance: { + type: 'json', + description: 'Current balance', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + availableBalance: { + type: 'json', + description: 'Available balance', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + accountNumber: { type: 'string', description: 'Bank account number' }, + routingNumber: { type: 'string', description: 'Bank routing number' }, + primary: { type: 'boolean', description: 'Whether this is the primary cash account' }, + }, +} diff --git a/apps/sim/tools/brex/get_company.ts b/apps/sim/tools/brex/get_company.ts new file mode 100644 index 0000000000..700e339a08 --- /dev/null +++ b/apps/sim/tools/brex/get_company.ts @@ -0,0 +1,53 @@ +import type { BrexApiKeyParams, BrexGetCompanyResponse } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexGetCompanyTool: ToolConfig = { + id: 'brex_get_company', + name: 'Brex Get Company', + description: 'Get the Brex company associated with the API token', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + }, + + request: { + url: `${BREX_API_BASE}/v2/company`, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + legalName: data.legal_name ?? '', + mailingAddress: data.mailing_address ?? null, + accountType: data.accountType ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique company ID' }, + legalName: { type: 'string', description: 'Legal name of the company' }, + mailingAddress: { + type: 'json', + description: 'Company mailing address (line1, line2, city, state, country, postal_code)', + optional: true, + }, + accountType: { + type: 'string', + description: 'Brex account type (BREX_CLASSIC or BREX_EMPOWER)', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/get_current_user.ts b/apps/sim/tools/brex/get_current_user.ts new file mode 100644 index 0000000000..73fa32dd1f --- /dev/null +++ b/apps/sim/tools/brex/get_current_user.ts @@ -0,0 +1,59 @@ +import type { BrexApiKeyParams, BrexGetUserResponse } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexGetCurrentUserTool: ToolConfig = { + id: 'brex_get_current_user', + name: 'Brex Get Current User', + description: 'Get the Brex user associated with the API token', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + }, + + request: { + url: `${BREX_API_BASE}/v2/users/me`, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + firstName: data.first_name ?? '', + lastName: data.last_name ?? '', + email: data.email ?? '', + status: data.status ?? null, + managerId: data.manager_id ?? null, + departmentId: data.department_id ?? null, + locationId: data.location_id ?? null, + titleId: data.title_id ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique user ID' }, + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Email address' }, + status: { + type: 'string', + description: 'User status (e.g., INVITED, ACTIVE, CLOSED, DISABLED)', + optional: true, + }, + managerId: { type: 'string', description: 'ID of the manager', optional: true }, + departmentId: { type: 'string', description: 'Department ID', optional: true }, + locationId: { type: 'string', description: 'Location ID', optional: true }, + titleId: { type: 'string', description: 'Title ID', optional: true }, + }, +} diff --git a/apps/sim/tools/brex/get_expense.ts b/apps/sim/tools/brex/get_expense.ts new file mode 100644 index 0000000000..7cc0c0c9d7 --- /dev/null +++ b/apps/sim/tools/brex/get_expense.ts @@ -0,0 +1,199 @@ +import type { BrexGetExpenseParams, BrexGetExpenseResponse } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +const EXPAND_FIELDS = [ + 'merchant', + 'user', + 'budget', + 'department', + 'location', + 'receipts.download_uris', +] + +export const brexGetExpenseTool: ToolConfig = { + id: 'brex_get_expense', + name: 'Brex Get Expense', + description: 'Get a single Brex expense by its ID, including merchant, user, and receipt details', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + expenseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the expense to fetch', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + for (const field of EXPAND_FIELDS) { + query.append('expand[]', field) + } + return `${BREX_API_BASE}/v1/expenses/${encodeURIComponent(params.expenseId.trim())}?${query.toString()}` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + memo: data.memo ?? null, + status: data.status ?? null, + paymentStatus: data.payment_status ?? null, + expenseType: data.expense_type ?? null, + category: data.category ?? null, + merchantId: data.merchant_id ?? null, + merchant: data.merchant ?? null, + budgetId: data.budget_id ?? null, + budget: data.budget ?? null, + departmentId: data.department_id ?? null, + department: data.department ?? null, + locationId: data.location_id ?? null, + location: data.location ?? null, + userId: data.user_id ?? null, + user: data.user ?? null, + originalAmount: data.original_amount ?? null, + billingAmount: data.billing_amount ?? null, + purchasedAmount: data.purchased_amount ?? null, + usdEquivalentAmount: data.usd_equivalent_amount ?? null, + purchasedAt: data.purchased_at ?? null, + updatedAt: data.updated_at ?? '', + paymentPostedAt: data.payment_posted_at ?? null, + receipts: data.receipts ?? [], + dashboardUrl: data.dashboard_url ?? '', + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique expense ID' }, + memo: { type: 'string', description: 'Memo on the expense', optional: true }, + status: { + type: 'string', + description: + 'Expense status (DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED)', + optional: true, + }, + paymentStatus: { + type: 'string', + description: + 'Payment status (NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED)', + optional: true, + }, + expenseType: { + type: 'string', + description: 'Expense type (CARD, BILLPAY, REIMBURSEMENT, CLAWBACK, UNSET)', + optional: true, + }, + category: { type: 'string', description: 'Merchant category of the expense', optional: true }, + merchantId: { type: 'string', description: 'Merchant ID', optional: true }, + merchant: { + type: 'json', + description: 'Merchant details (raw descriptor, MCC, country)', + optional: true, + properties: { + raw_descriptor: { type: 'string', description: 'Raw merchant descriptor' }, + mcc: { type: 'string', description: 'Merchant category code' }, + country: { type: 'string', description: 'Merchant country' }, + }, + }, + budgetId: { type: 'string', description: 'Budget ID', optional: true }, + budget: { + type: 'json', + description: 'Budget the expense belongs to', + optional: true, + properties: { + id: { type: 'string', description: 'Budget ID' }, + name: { type: 'string', description: 'Budget name' }, + }, + }, + departmentId: { type: 'string', description: 'Department ID', optional: true }, + department: { + type: 'json', + description: 'Department of the expense owner', + optional: true, + properties: { + id: { type: 'string', description: 'Department ID' }, + name: { type: 'string', description: 'Department name' }, + }, + }, + locationId: { type: 'string', description: 'Location ID', optional: true }, + location: { + type: 'json', + description: 'Location of the expense owner', + optional: true, + properties: { + id: { type: 'string', description: 'Location ID' }, + name: { type: 'string', description: 'Location name' }, + }, + }, + userId: { type: 'string', description: 'ID of the user who made the expense', optional: true }, + user: { + type: 'json', + description: 'User who made the expense', + optional: true, + properties: { + id: { type: 'string', description: 'User ID' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + }, + }, + originalAmount: { + type: 'json', + description: 'Original transaction amount', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + billingAmount: { + type: 'json', + description: 'Amount billed to the account', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + purchasedAmount: { + type: 'json', + description: 'Amount at the time of purchase', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + usdEquivalentAmount: { + type: 'json', + description: 'USD equivalent amount', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + purchasedAt: { type: 'string', description: 'Purchase timestamp (ISO 8601)', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp (ISO 8601)' }, + paymentPostedAt: { + type: 'string', + description: 'Timestamp the payment was posted (ISO 8601)', + optional: true, + }, + receipts: { + type: 'array', + description: 'Receipts attached to the expense', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Receipt ID' }, + download_uris: { type: 'array', description: 'Pre-signed receipt download URLs' }, + }, + }, + }, + dashboardUrl: { type: 'string', description: 'Link to the expense in the Brex dashboard' }, + }, +} diff --git a/apps/sim/tools/brex/get_spend_limit.ts b/apps/sim/tools/brex/get_spend_limit.ts new file mode 100644 index 0000000000..20144d1d0e --- /dev/null +++ b/apps/sim/tools/brex/get_spend_limit.ts @@ -0,0 +1,89 @@ +import type { BrexGetSpendLimitParams, BrexGetSpendLimitResponse } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexGetSpendLimitTool: ToolConfig = + { + id: 'brex_get_spend_limit', + name: 'Brex Get Spend Limit', + description: 'Get a Brex spend limit by its ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + spendLimitId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the spend limit to fetch', + }, + }, + + request: { + url: (params) => + `${BREX_API_BASE}/v2/spend_limits/${encodeURIComponent(params.spendLimitId.trim())}`, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + accountId: data.account_id ?? '', + name: data.name ?? '', + description: data.description ?? null, + parentBudgetId: data.parent_budget_id ?? null, + status: data.status ?? '', + periodRecurrenceType: data.period_recurrence_type ?? '', + spendType: data.spend_type ?? '', + startDate: data.start_date ?? null, + endDate: data.end_date ?? null, + ownerUserIds: data.owner_user_ids ?? [], + memberUserIds: data.member_user_ids ?? [], + currentPeriodBalance: data.current_period_balance ?? null, + authorizationSettings: data.authorization_settings ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique spend limit ID' }, + accountId: { type: 'string', description: 'Account ID the spend limit belongs to' }, + name: { type: 'string', description: 'Spend limit name' }, + description: { type: 'string', description: 'Spend limit description', optional: true }, + parentBudgetId: { type: 'string', description: 'Parent budget ID', optional: true }, + status: { + type: 'string', + description: 'Spend limit status (ACTIVE, EXPIRED, ARCHIVED, DELETED)', + }, + periodRecurrenceType: { + type: 'string', + description: 'Period recurrence (PER_WEEK, PER_MONTH, PER_QUARTER, PER_YEAR, ONE_TIME)', + }, + spendType: { type: 'string', description: 'Spend type of the limit' }, + startDate: { type: 'string', description: 'Spend limit start date', optional: true }, + endDate: { type: 'string', description: 'Spend limit end date', optional: true }, + ownerUserIds: { type: 'array', description: 'User IDs of the spend limit owners' }, + memberUserIds: { type: 'array', description: 'User IDs of the spend limit members' }, + currentPeriodBalance: { + type: 'json', + description: 'Balance for the current period', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + authorizationSettings: { + type: 'json', + description: 'Authorization settings (base limit, authorization type, rollover refresh)', + optional: true, + }, + }, + } diff --git a/apps/sim/tools/brex/get_transfer.ts b/apps/sim/tools/brex/get_transfer.ts new file mode 100644 index 0000000000..9328f6f9bc --- /dev/null +++ b/apps/sim/tools/brex/get_transfer.ts @@ -0,0 +1,100 @@ +import type { BrexGetTransferParams, BrexGetTransferResponse } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexGetTransferTool: ToolConfig = { + id: 'brex_get_transfer', + name: 'Brex Get Transfer', + description: 'Get a Brex money transfer by its ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + transferId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the transfer to fetch', + }, + }, + + request: { + url: (params) => + `${BREX_API_BASE}/v1/transfers/${encodeURIComponent(params.transferId.trim())}`, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + counterparty: data.counterparty ?? null, + description: data.description ?? null, + paymentType: data.payment_type ?? '', + amount: data.amount ?? null, + processDate: data.process_date ?? null, + originatingAccount: data.originating_account ?? null, + status: data.status ?? '', + cancellationReason: data.cancellation_reason ?? null, + estimatedDeliveryDate: data.estimated_delivery_date ?? null, + creatorUserId: data.creator_user_id ?? null, + createdAt: data.created_at ?? null, + displayName: data.display_name ?? null, + externalMemo: data.external_memo ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique transfer ID' }, + counterparty: { type: 'json', description: 'Transfer counterparty details', optional: true }, + description: { type: 'string', description: 'Transfer description', optional: true }, + paymentType: { + type: 'string', + description: 'Payment type (ACH, DOMESTIC_WIRE, CHEQUE, INTERNATIONAL_WIRE, BOOK_TRANSFER)', + }, + amount: { + type: 'json', + description: 'Transfer amount', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + processDate: { type: 'string', description: 'Date the transfer processes', optional: true }, + originatingAccount: { + type: 'json', + description: 'Account the transfer originates from', + optional: true, + }, + status: { + type: 'string', + description: 'Transfer status (PROCESSING, SCHEDULED, PENDING_APPROVAL, FAILED, PROCESSED)', + }, + cancellationReason: { + type: 'string', + description: 'Reason the transfer was canceled', + optional: true, + }, + estimatedDeliveryDate: { + type: 'string', + description: 'Estimated delivery date', + optional: true, + }, + creatorUserId: { + type: 'string', + description: 'ID of the user who created the transfer', + optional: true, + }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + displayName: { type: 'string', description: 'Transfer display name', optional: true }, + externalMemo: { type: 'string', description: 'External memo', optional: true }, + }, +} diff --git a/apps/sim/tools/brex/get_user.ts b/apps/sim/tools/brex/get_user.ts new file mode 100644 index 0000000000..ccf972151e --- /dev/null +++ b/apps/sim/tools/brex/get_user.ts @@ -0,0 +1,65 @@ +import type { BrexGetUserParams, BrexGetUserResponse } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexGetUserTool: ToolConfig = { + id: 'brex_get_user', + name: 'Brex Get User', + description: 'Get a Brex user by their ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the user to fetch', + }, + }, + + request: { + url: (params) => `${BREX_API_BASE}/v2/users/${encodeURIComponent(params.userId.trim())}`, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + firstName: data.first_name ?? '', + lastName: data.last_name ?? '', + email: data.email ?? '', + status: data.status ?? null, + managerId: data.manager_id ?? null, + departmentId: data.department_id ?? null, + locationId: data.location_id ?? null, + titleId: data.title_id ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique user ID' }, + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Email address' }, + status: { + type: 'string', + description: 'User status (e.g., INVITED, ACTIVE, CLOSED, DISABLED)', + optional: true, + }, + managerId: { type: 'string', description: 'ID of the manager', optional: true }, + departmentId: { type: 'string', description: 'Department ID', optional: true }, + locationId: { type: 'string', description: 'Location ID', optional: true }, + titleId: { type: 'string', description: 'Title ID', optional: true }, + }, +} diff --git a/apps/sim/tools/brex/get_vendor.ts b/apps/sim/tools/brex/get_vendor.ts new file mode 100644 index 0000000000..ad13edfbee --- /dev/null +++ b/apps/sim/tools/brex/get_vendor.ts @@ -0,0 +1,56 @@ +import type { BrexGetVendorParams, BrexGetVendorResponse } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexGetVendorTool: ToolConfig = { + id: 'brex_get_vendor', + name: 'Brex Get Vendor', + description: 'Get a Brex vendor by its ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + vendorId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the vendor to fetch', + }, + }, + + request: { + url: (params) => `${BREX_API_BASE}/v1/vendors/${encodeURIComponent(params.vendorId.trim())}`, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + companyName: data.company_name ?? null, + email: data.email ?? null, + phone: data.phone ?? null, + paymentAccounts: data.payment_accounts ?? [], + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique vendor ID' }, + companyName: { type: 'string', description: 'Vendor company name', optional: true }, + email: { type: 'string', description: 'Vendor email address', optional: true }, + phone: { type: 'string', description: 'Vendor phone number', optional: true }, + paymentAccounts: { + type: 'array', + description: 'Payment accounts associated with the vendor', + }, + }, +} diff --git a/apps/sim/tools/brex/index.ts b/apps/sim/tools/brex/index.ts new file mode 100644 index 0000000000..4bc1194ee0 --- /dev/null +++ b/apps/sim/tools/brex/index.ts @@ -0,0 +1,28 @@ +export { brexGetBudgetTool } from '@/tools/brex/get_budget' +export { brexGetCashAccountTool } from '@/tools/brex/get_cash_account' +export { brexGetCompanyTool } from '@/tools/brex/get_company' +export { brexGetCurrentUserTool } from '@/tools/brex/get_current_user' +export { brexGetExpenseTool } from '@/tools/brex/get_expense' +export { brexGetSpendLimitTool } from '@/tools/brex/get_spend_limit' +export { brexGetTransferTool } from '@/tools/brex/get_transfer' +export { brexGetUserTool } from '@/tools/brex/get_user' +export { brexGetVendorTool } from '@/tools/brex/get_vendor' +export { brexListBudgetsTool } from '@/tools/brex/list_budgets' +export { brexListCardAccountsTool } from '@/tools/brex/list_card_accounts' +export { brexListCardStatementsTool } from '@/tools/brex/list_card_statements' +export { brexListCardTransactionsTool } from '@/tools/brex/list_card_transactions' +export { brexListCardsTool } from '@/tools/brex/list_cards' +export { brexListCashAccountsTool } from '@/tools/brex/list_cash_accounts' +export { brexListCashStatementsTool } from '@/tools/brex/list_cash_statements' +export { brexListCashTransactionsTool } from '@/tools/brex/list_cash_transactions' +export { brexListDepartmentsTool } from '@/tools/brex/list_departments' +export { brexListExpensesTool } from '@/tools/brex/list_expenses' +export { brexListLocationsTool } from '@/tools/brex/list_locations' +export { brexListSpendLimitsTool } from '@/tools/brex/list_spend_limits' +export { brexListTitlesTool } from '@/tools/brex/list_titles' +export { brexListTransfersTool } from '@/tools/brex/list_transfers' +export { brexListUsersTool } from '@/tools/brex/list_users' +export { brexListVendorsTool } from '@/tools/brex/list_vendors' +export { brexMatchReceiptTool } from '@/tools/brex/match_receipt' +export { brexUpdateExpenseTool } from '@/tools/brex/update_expense' +export { brexUploadReceiptTool } from '@/tools/brex/upload_receipt' diff --git a/apps/sim/tools/brex/list_budgets.ts b/apps/sim/tools/brex/list_budgets.ts new file mode 100644 index 0000000000..1da6bf6631 --- /dev/null +++ b/apps/sim/tools/brex/list_budgets.ts @@ -0,0 +1,98 @@ +import type { BrexListBudgetsResponse, BrexPaginationParams } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListBudgetsTool: ToolConfig = { + id: 'brex_list_budgets', + name: 'Brex List Budgets', + description: 'List budgets in the Brex account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of budgets to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + appendBrexPagination(query, params) + const queryString = query.toString() + return queryString + ? `${BREX_API_BASE}/v2/budgets?${queryString}` + : `${BREX_API_BASE}/v2/budgets` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Budgets in the Brex account', + items: { + type: 'json', + properties: { + budget_id: { type: 'string', description: 'Unique budget ID' }, + account_id: { type: 'string', description: 'Account ID the budget belongs to' }, + name: { type: 'string', description: 'Budget name' }, + description: { type: 'string', description: 'Budget description', optional: true }, + parent_budget_id: { type: 'string', description: 'Parent budget ID', optional: true }, + owner_user_ids: { type: 'array', description: 'User IDs of the budget owners' }, + period_recurrence_type: { + type: 'string', + description: 'Budget period recurrence (e.g., MONTHLY, QUARTERLY, YEARLY, ONE_TIME)', + }, + start_date: { type: 'string', description: 'Budget start date', optional: true }, + end_date: { type: 'string', description: 'Budget end date', optional: true }, + amount: { + type: 'json', + description: 'Budget amount', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + spend_budget_status: { type: 'string', description: 'Budget status' }, + limit_type: { type: 'string', description: 'Budget limit type', optional: true }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_card_accounts.ts b/apps/sim/tools/brex/list_card_accounts.ts new file mode 100644 index 0000000000..24c336546f --- /dev/null +++ b/apps/sim/tools/brex/list_card_accounts.ts @@ -0,0 +1,73 @@ +import type { BrexApiKeyParams, BrexListCardAccountsResponse } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListCardAccountsTool: ToolConfig = + { + id: 'brex_list_card_accounts', + name: 'Brex List Card Accounts', + description: 'List all Brex card accounts with balances and limits', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + }, + + request: { + url: `${BREX_API_BASE}/v2/accounts/card`, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + accounts: Array.isArray(data) ? data : [], + }, + } + }, + + outputs: { + accounts: { + type: 'array', + description: 'Card accounts', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Unique account ID' }, + status: { type: 'string', description: 'Account status', optional: true }, + current_balance: { + type: 'json', + description: 'Current balance', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + available_balance: { + type: 'json', + description: 'Available balance', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + account_limit: { + type: 'json', + description: 'Account limit', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + current_statement_period: { + type: 'json', + description: 'Current statement period (start_date, end_date)', + }, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/brex/list_card_statements.ts b/apps/sim/tools/brex/list_card_statements.ts new file mode 100644 index 0000000000..dfe9308ed4 --- /dev/null +++ b/apps/sim/tools/brex/list_card_statements.ts @@ -0,0 +1,95 @@ +import type { BrexListStatementsResponse, BrexPaginationParams } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListCardStatementsTool: ToolConfig< + BrexPaginationParams, + BrexListStatementsResponse +> = { + id: 'brex_list_card_statements', + name: 'Brex List Card Statements', + description: 'List finalized statements for the primary Brex card account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of statements to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + appendBrexPagination(query, params) + const queryString = query.toString() + return queryString + ? `${BREX_API_BASE}/v2/accounts/card/primary/statements?${queryString}` + : `${BREX_API_BASE}/v2/accounts/card/primary/statements` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Finalized card account statements', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Unique statement ID' }, + start_balance: { + type: 'json', + description: 'Balance at the start of the period', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + end_balance: { + type: 'json', + description: 'Balance at the end of the period', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + period: { type: 'json', description: 'Statement period (start_date, end_date)' }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_card_transactions.ts b/apps/sim/tools/brex/list_card_transactions.ts new file mode 100644 index 0000000000..7cb34432b6 --- /dev/null +++ b/apps/sim/tools/brex/list_card_transactions.ts @@ -0,0 +1,93 @@ +import type { + BrexListCardTransactionsParams, + BrexListCardTransactionsResponse, +} from '@/tools/brex/types' +import { BREX_CARD_TRANSACTION_PROPERTIES } from '@/tools/brex/types' +import { + appendBrexArrayParam, + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListCardTransactionsTool: ToolConfig< + BrexListCardTransactionsParams, + BrexListCardTransactionsResponse +> = { + id: 'brex_list_card_transactions', + name: 'Brex List Card Transactions', + description: 'List settled card transactions for all Brex card accounts', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + userIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated user IDs to filter transactions by cardholder', + }, + postedAtStart: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only include transactions posted at or after this ISO 8601 timestamp', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of transactions to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + query.append('expand[]', 'expense_id') + appendBrexArrayParam(query, 'user_ids', params.userIds) + if (params.postedAtStart) query.append('posted_at_start', params.postedAtStart) + appendBrexPagination(query, params) + return `${BREX_API_BASE}/v2/transactions/card/primary?${query.toString()}` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Settled card transactions', + items: { type: 'json', properties: BREX_CARD_TRANSACTION_PROPERTIES }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_cards.ts b/apps/sim/tools/brex/list_cards.ts new file mode 100644 index 0000000000..0c05121d18 --- /dev/null +++ b/apps/sim/tools/brex/list_cards.ts @@ -0,0 +1,101 @@ +import type { BrexListCardsParams, BrexListCardsResponse } from '@/tools/brex/types' +import { + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListCardsTool: ToolConfig = { + id: 'brex_list_cards', + name: 'Brex List Cards', + description: 'List cards in the Brex account, optionally filtered by card owner', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter cards by the ID of the card owner', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of cards to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.userId) query.append('user_id', params.userId.trim()) + appendBrexPagination(query, params) + const queryString = query.toString() + return queryString ? `${BREX_API_BASE}/v2/cards?${queryString}` : `${BREX_API_BASE}/v2/cards` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Cards in the Brex account', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Unique card ID' }, + owner: { type: 'json', description: 'Card owner (type, user_id)' }, + status: { type: 'string', description: 'Card status', optional: true }, + last_four: { type: 'string', description: 'Last four digits of the card number' }, + card_name: { type: 'string', description: 'Card name' }, + card_type: { + type: 'string', + description: 'Card type (VIRTUAL or PHYSICAL)', + optional: true, + }, + limit_type: { type: 'string', description: 'Limit type (CARD or USER)' }, + spend_controls: { + type: 'json', + description: 'Spend controls on the card', + optional: true, + }, + billing_address: { type: 'json', description: 'Billing address of the card' }, + expiration_date: { type: 'json', description: 'Card expiration date (month, year)' }, + budget_id: { type: 'string', description: 'Associated budget ID', optional: true }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_cash_accounts.ts b/apps/sim/tools/brex/list_cash_accounts.ts new file mode 100644 index 0000000000..6b66758d20 --- /dev/null +++ b/apps/sim/tools/brex/list_cash_accounts.ts @@ -0,0 +1,99 @@ +import type { BrexListCashAccountsResponse, BrexPaginationParams } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListCashAccountsTool: ToolConfig< + BrexPaginationParams, + BrexListCashAccountsResponse +> = { + id: 'brex_list_cash_accounts', + name: 'Brex List Cash Accounts', + description: 'List all Brex cash accounts with balances and account details', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of accounts to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + appendBrexPagination(query, params) + const queryString = query.toString() + return queryString + ? `${BREX_API_BASE}/v2/accounts/cash?${queryString}` + : `${BREX_API_BASE}/v2/accounts/cash` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Cash accounts', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Unique account ID' }, + name: { type: 'string', description: 'Account name' }, + status: { type: 'string', description: 'Account status', optional: true }, + current_balance: { + type: 'json', + description: 'Current balance', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + available_balance: { + type: 'json', + description: 'Available balance', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + account_number: { type: 'string', description: 'Bank account number' }, + routing_number: { type: 'string', description: 'Bank routing number' }, + primary: { type: 'boolean', description: 'Whether this is the primary cash account' }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_cash_statements.ts b/apps/sim/tools/brex/list_cash_statements.ts new file mode 100644 index 0000000000..bb506b446a --- /dev/null +++ b/apps/sim/tools/brex/list_cash_statements.ts @@ -0,0 +1,100 @@ +import type { BrexListCashStatementsParams, BrexListStatementsResponse } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListCashStatementsTool: ToolConfig< + BrexListCashStatementsParams, + BrexListStatementsResponse +> = { + id: 'brex_list_cash_statements', + name: 'Brex List Cash Statements', + description: 'List finalized statements for a Brex cash account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the cash account to list statements for', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of statements to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + appendBrexPagination(query, params) + const queryString = query.toString() + const base = `${BREX_API_BASE}/v2/accounts/cash/${encodeURIComponent(params.accountId.trim())}/statements` + return queryString ? `${base}?${queryString}` : base + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Finalized cash account statements', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Unique statement ID' }, + start_balance: { + type: 'json', + description: 'Balance at the start of the period', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + end_balance: { + type: 'json', + description: 'Balance at the end of the period', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + period: { type: 'json', description: 'Statement period (start_date, end_date)' }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_cash_transactions.ts b/apps/sim/tools/brex/list_cash_transactions.ts new file mode 100644 index 0000000000..0d0237e696 --- /dev/null +++ b/apps/sim/tools/brex/list_cash_transactions.ts @@ -0,0 +1,92 @@ +import type { + BrexListCashTransactionsParams, + BrexListCashTransactionsResponse, +} from '@/tools/brex/types' +import { BREX_CASH_TRANSACTION_PROPERTIES } from '@/tools/brex/types' +import { + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListCashTransactionsTool: ToolConfig< + BrexListCashTransactionsParams, + BrexListCashTransactionsResponse +> = { + id: 'brex_list_cash_transactions', + name: 'Brex List Cash Transactions', + description: 'List transactions for a Brex cash account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the cash account to list transactions for', + }, + postedAtStart: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only include transactions posted at or after this ISO 8601 timestamp', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of transactions to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.postedAtStart) query.append('posted_at_start', params.postedAtStart) + appendBrexPagination(query, params) + const queryString = query.toString() + const base = `${BREX_API_BASE}/v2/transactions/cash/${encodeURIComponent(params.accountId.trim())}` + return queryString ? `${base}?${queryString}` : base + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Cash account transactions', + items: { type: 'json', properties: BREX_CASH_TRANSACTION_PROPERTIES }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_departments.ts b/apps/sim/tools/brex/list_departments.ts new file mode 100644 index 0000000000..ce8916326f --- /dev/null +++ b/apps/sim/tools/brex/list_departments.ts @@ -0,0 +1,90 @@ +import type { BrexListDepartmentsResponse, BrexNameFilterParams } from '@/tools/brex/types' +import { + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListDepartmentsTool: ToolConfig< + BrexNameFilterParams, + BrexListDepartmentsResponse +> = { + id: 'brex_list_departments', + name: 'Brex List Departments', + description: 'List departments in the Brex account, optionally filtered by name', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter departments by name', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of departments to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.name) query.append('name', params.name.trim()) + appendBrexPagination(query, params) + const queryString = query.toString() + return queryString + ? `${BREX_API_BASE}/v2/departments?${queryString}` + : `${BREX_API_BASE}/v2/departments` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Departments in the Brex account', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Unique department ID' }, + name: { type: 'string', description: 'Department name' }, + description: { type: 'string', description: 'Department description', optional: true }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_expenses.ts b/apps/sim/tools/brex/list_expenses.ts new file mode 100644 index 0000000000..021604cfb9 --- /dev/null +++ b/apps/sim/tools/brex/list_expenses.ts @@ -0,0 +1,122 @@ +import type { BrexListExpensesParams, BrexListExpensesResponse } from '@/tools/brex/types' +import { BREX_EXPENSE_ITEM_PROPERTIES } from '@/tools/brex/types' +import { + appendBrexArrayParam, + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +const EXPAND_FIELDS = [ + 'merchant', + 'user', + 'budget', + 'department', + 'location', + 'receipts.download_uris', +] + +export const brexListExpensesTool: ToolConfig = { + id: 'brex_list_expenses', + name: 'Brex List Expenses', + description: + 'List expenses in the Brex account with optional filters for user, status, payment status, and purchase date range', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + userIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated user IDs to filter expenses by owner', + }, + statuses: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated expense statuses to filter by: DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED', + }, + paymentStatuses: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated payment statuses to filter by: NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED', + }, + purchasedAtStart: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only include expenses purchased at or after this ISO 8601 timestamp', + }, + purchasedAtEnd: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only include expenses purchased before this ISO 8601 timestamp', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of expenses to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + for (const field of EXPAND_FIELDS) { + query.append('expand[]', field) + } + appendBrexArrayParam(query, 'user_id[]', params.userIds) + appendBrexArrayParam(query, 'status[]', params.statuses) + appendBrexArrayParam(query, 'payment_status[]', params.paymentStatuses) + if (params.purchasedAtStart) query.append('purchased_at_start', params.purchasedAtStart) + if (params.purchasedAtEnd) query.append('purchased_at_end', params.purchasedAtEnd) + appendBrexPagination(query, params) + return `${BREX_API_BASE}/v1/expenses?${query.toString()}` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Expenses matching the filters', + items: { type: 'json', properties: BREX_EXPENSE_ITEM_PROPERTIES }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_locations.ts b/apps/sim/tools/brex/list_locations.ts new file mode 100644 index 0000000000..1d8219b7ce --- /dev/null +++ b/apps/sim/tools/brex/list_locations.ts @@ -0,0 +1,87 @@ +import type { BrexListLocationsResponse, BrexNameFilterParams } from '@/tools/brex/types' +import { + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListLocationsTool: ToolConfig = { + id: 'brex_list_locations', + name: 'Brex List Locations', + description: 'List locations in the Brex account, optionally filtered by name', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter locations by name', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of locations to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.name) query.append('name', params.name.trim()) + appendBrexPagination(query, params) + const queryString = query.toString() + return queryString + ? `${BREX_API_BASE}/v2/locations?${queryString}` + : `${BREX_API_BASE}/v2/locations` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Locations in the Brex account', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Unique location ID' }, + name: { type: 'string', description: 'Location name' }, + description: { type: 'string', description: 'Location description', optional: true }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_spend_limits.ts b/apps/sim/tools/brex/list_spend_limits.ts new file mode 100644 index 0000000000..ba69605eab --- /dev/null +++ b/apps/sim/tools/brex/list_spend_limits.ts @@ -0,0 +1,108 @@ +import type { BrexListSpendLimitsParams, BrexListSpendLimitsResponse } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { + appendBrexArrayParam, + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListSpendLimitsTool: ToolConfig< + BrexListSpendLimitsParams, + BrexListSpendLimitsResponse +> = { + id: 'brex_list_spend_limits', + name: 'Brex List Spend Limits', + description: 'List spend limits in the Brex account, optionally filtered by member user', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + memberUserIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated user IDs to filter spend limits by member', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of spend limits to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + appendBrexArrayParam(query, 'member_user_id[]', params.memberUserIds) + appendBrexPagination(query, params) + const queryString = query.toString() + return queryString + ? `${BREX_API_BASE}/v2/spend_limits?${queryString}` + : `${BREX_API_BASE}/v2/spend_limits` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Spend limits in the Brex account', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Unique spend limit ID' }, + account_id: { type: 'string', description: 'Account ID the spend limit belongs to' }, + name: { type: 'string', description: 'Spend limit name' }, + description: { type: 'string', description: 'Spend limit description', optional: true }, + parent_budget_id: { type: 'string', description: 'Parent budget ID', optional: true }, + status: { type: 'string', description: 'Spend limit status' }, + period_recurrence_type: { + type: 'string', + description: 'Period recurrence (e.g., MONTHLY, QUARTERLY, YEARLY, ONE_TIME)', + }, + spend_type: { type: 'string', description: 'Spend type of the limit' }, + owner_user_ids: { type: 'array', description: 'User IDs of the spend limit owners' }, + member_user_ids: { type: 'array', description: 'User IDs of the spend limit members' }, + current_period_balance: { + type: 'json', + description: 'Balance for the current period', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_titles.ts b/apps/sim/tools/brex/list_titles.ts new file mode 100644 index 0000000000..94d5025310 --- /dev/null +++ b/apps/sim/tools/brex/list_titles.ts @@ -0,0 +1,86 @@ +import type { BrexListTitlesResponse, BrexNameFilterParams } from '@/tools/brex/types' +import { + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListTitlesTool: ToolConfig = { + id: 'brex_list_titles', + name: 'Brex List Titles', + description: 'List job titles in the Brex account, optionally filtered by name', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter titles by name', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of titles to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.name) query.append('name', params.name.trim()) + appendBrexPagination(query, params) + const queryString = query.toString() + return queryString + ? `${BREX_API_BASE}/v2/titles?${queryString}` + : `${BREX_API_BASE}/v2/titles` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Job titles in the Brex account', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Unique title ID' }, + name: { type: 'string', description: 'Title name' }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_transfers.ts b/apps/sim/tools/brex/list_transfers.ts new file mode 100644 index 0000000000..1ec2f3cbdb --- /dev/null +++ b/apps/sim/tools/brex/list_transfers.ts @@ -0,0 +1,125 @@ +import type { BrexListTransfersResponse, BrexPaginationParams } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListTransfersTool: ToolConfig = { + id: 'brex_list_transfers', + name: 'Brex List Transfers', + description: 'List money transfers in the Brex account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of transfers to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + appendBrexPagination(query, params) + const queryString = query.toString() + return queryString + ? `${BREX_API_BASE}/v1/transfers?${queryString}` + : `${BREX_API_BASE}/v1/transfers` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Transfers in the Brex account', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Unique transfer ID' }, + counterparty: { + type: 'json', + description: 'Transfer counterparty details', + optional: true, + }, + description: { type: 'string', description: 'Transfer description', optional: true }, + payment_type: { + type: 'string', + description: 'Payment type (e.g., ACH, DOMESTIC_WIRE, CHEQUE, INTERNATIONAL_WIRE)', + }, + amount: { + type: 'json', + description: 'Transfer amount', + properties: BREX_MONEY_PROPERTIES, + }, + process_date: { + type: 'string', + description: 'Date the transfer processes', + optional: true, + }, + originating_account: { + type: 'json', + description: 'Account the transfer originates from', + }, + status: { + type: 'string', + description: 'Transfer status (e.g., SCHEDULED, PROCESSING, COMPLETED, FAILED)', + }, + cancellation_reason: { + type: 'string', + description: 'Reason the transfer was canceled', + optional: true, + }, + estimated_delivery_date: { + type: 'string', + description: 'Estimated delivery date', + optional: true, + }, + creator_user_id: { + type: 'string', + description: 'ID of the user who created the transfer', + optional: true, + }, + created_at: { type: 'string', description: 'Creation timestamp', optional: true }, + display_name: { type: 'string', description: 'Transfer display name', optional: true }, + external_memo: { type: 'string', description: 'External memo', optional: true }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_users.ts b/apps/sim/tools/brex/list_users.ts new file mode 100644 index 0000000000..1c917a730c --- /dev/null +++ b/apps/sim/tools/brex/list_users.ts @@ -0,0 +1,79 @@ +import type { BrexListUsersParams, BrexListUsersResponse } from '@/tools/brex/types' +import { BREX_USER_PROPERTIES } from '@/tools/brex/types' +import { + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListUsersTool: ToolConfig = { + id: 'brex_list_users', + name: 'Brex List Users', + description: 'List users in the Brex account, optionally filtered by email', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter users by exact email address', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of users to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.email) query.append('email', params.email.trim()) + appendBrexPagination(query, params) + const queryString = query.toString() + return queryString ? `${BREX_API_BASE}/v2/users?${queryString}` : `${BREX_API_BASE}/v2/users` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Users in the Brex account', + items: { type: 'json', properties: BREX_USER_PROPERTIES }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/list_vendors.ts b/apps/sim/tools/brex/list_vendors.ts new file mode 100644 index 0000000000..5508f039ca --- /dev/null +++ b/apps/sim/tools/brex/list_vendors.ts @@ -0,0 +1,93 @@ +import type { BrexListVendorsResponse, BrexNameFilterParams } from '@/tools/brex/types' +import { + appendBrexPagination, + BREX_API_BASE, + buildBrexHeaders, + parseBrexJson, +} from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexListVendorsTool: ToolConfig = { + id: 'brex_list_vendors', + name: 'Brex List Vendors', + description: 'List vendors in the Brex account, optionally filtered by name', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter vendors by name', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Number of vendors to return (default 100, max 1000)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.name) query.append('name', params.name.trim()) + appendBrexPagination(query, params) + const queryString = query.toString() + return queryString + ? `${BREX_API_BASE}/v1/vendors?${queryString}` + : `${BREX_API_BASE}/v1/vendors` + }, + method: 'GET', + headers: (params) => buildBrexHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + items: data.items ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Vendors in the Brex account', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Unique vendor ID' }, + company_name: { type: 'string', description: 'Vendor company name', optional: true }, + email: { type: 'string', description: 'Vendor email address', optional: true }, + phone: { type: 'string', description: 'Vendor phone number', optional: true }, + payment_accounts: { + type: 'array', + description: 'Payment accounts associated with the vendor', + optional: true, + }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/match_receipt.ts b/apps/sim/tools/brex/match_receipt.ts new file mode 100644 index 0000000000..621cb5e6b7 --- /dev/null +++ b/apps/sim/tools/brex/match_receipt.ts @@ -0,0 +1,62 @@ +import type { BrexMatchReceiptParams, BrexUploadReceiptResponse } from '@/tools/brex/types' +import type { ToolConfig } from '@/tools/types' + +export const brexMatchReceiptTool: ToolConfig = { + id: 'brex_match_receipt', + name: 'Brex Match Receipt', + description: 'Upload a receipt file and let Brex automatically match it with existing expenses', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + file: { + type: 'file', + required: true, + visibility: 'user-or-llm', + description: 'Receipt file to upload (max 50 MB)', + }, + receiptName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Receipt file name including extension (defaults to the uploaded file name)', + }, + }, + + request: { + url: '/api/tools/brex/upload-receipt', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + apiKey: params.apiKey, + file: params.file, + receiptName: params.receiptName, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!data.success) { + throw new Error(data.error || 'Failed to match receipt') + } + return { + success: true, + output: data.output, + } + }, + + outputs: { + receiptId: { type: 'string', description: 'Unique identifier of the receipt match request' }, + receiptName: { type: 'string', description: 'Name the receipt was uploaded with' }, + expenseId: { + type: 'string', + description: 'Always null for receipt match (Brex matches the receipt asynchronously)', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/brex/types.ts b/apps/sim/tools/brex/types.ts new file mode 100644 index 0000000000..0faa39df93 --- /dev/null +++ b/apps/sim/tools/brex/types.ts @@ -0,0 +1,749 @@ +import type { OutputProperty, ToolResponse } from '@/tools/types' + +export interface BrexPaginationParams { + apiKey: string + cursor?: string + limit?: string +} + +export interface BrexMoney { + amount: number + currency: string | null +} + +export interface BrexExpenseReceipt { + id: string + download_uris?: string[] +} + +export interface BrexExpense { + id: string + memo: string | null + status: string | null + payment_status: string | null + expense_type: string | null + category: string | null + merchant_id: string | null + merchant: { raw_descriptor: string; mcc: string; country: string } | null + budget_id: string | null + budget: { id: string; name: string } | null + department_id: string | null + department: { id: string; name: string } | null + location_id: string | null + location: { id: string; name: string } | null + user_id: string | null + user: { id: string; first_name: string; last_name: string } | null + original_amount: BrexMoney | null + billing_amount: BrexMoney | null + purchased_amount: BrexMoney | null + usd_equivalent_amount: BrexMoney | null + purchased_at: string | null + updated_at: string + payment_posted_at: string | null + receipts: BrexExpenseReceipt[] + dashboard_url: string +} + +export interface BrexCardTransaction { + id: string + card_id: string | null + description: string + amount: BrexMoney + initiated_at_date: string + posted_at_date: string + type: string | null + merchant: { raw_descriptor: string; mcc: string; country: string } | null + expense_id: string | null +} + +export interface BrexCashTransaction { + id: string + description: string + amount: BrexMoney | null + initiated_at_date: string + posted_at_date: string + type: string | null + transfer_id: string | null +} + +export interface BrexCardAccount { + id: string + status: string | null + current_balance: BrexMoney | null + available_balance: BrexMoney | null + account_limit: BrexMoney | null + current_statement_period: { start_date: string; end_date: string } +} + +export interface BrexCashAccount { + id: string + name: string + status: string | null + current_balance: BrexMoney | null + available_balance: BrexMoney | null + account_number: string + routing_number: string + primary: boolean +} + +export interface BrexUser { + id: string + first_name: string + last_name: string + email: string + status: string | null + manager_id: string | null + department_id: string | null + location_id: string | null + title_id: string | null +} + +export interface BrexDepartment { + id: string + name: string + description: string | null +} + +export interface BrexLocation { + id: string + name: string + description: string | null +} + +export interface BrexBudget { + budget_id: string + account_id: string + name: string + description: string | null + parent_budget_id: string | null + owner_user_ids: string[] + period_recurrence_type: string + start_date: string | null + end_date: string | null + amount: BrexMoney | null + spend_budget_status: string + limit_type: string | null +} + +export interface BrexSpendLimit { + id: string + account_id: string + name: string + description: string | null + parent_budget_id: string | null + status: string + period_recurrence_type: string + spend_type: string + owner_user_ids: string[] + member_user_ids: string[] + current_period_balance: BrexMoney | null +} + +export interface BrexVendor { + id: string + company_name: string | null + email: string | null + phone: string | null + payment_accounts: unknown[] +} + +export interface BrexTransfer { + id: string + counterparty: Record | null + description: string | null + payment_type: string + amount: BrexMoney + process_date: string | null + originating_account: Record + status: string + cancellation_reason: string | null + estimated_delivery_date: string | null + creator_user_id: string | null + created_at: string | null + display_name: string | null + external_memo: string | null +} + +export interface BrexCard { + id: string + owner: Record + status: string | null + last_four: string + card_name: string + card_type: string | null + limit_type: string + spend_controls: Record | null + billing_address: Record + expiration_date: Record + budget_id: string | null +} + +export interface BrexStatement { + id: string + start_balance: BrexMoney | null + end_balance: BrexMoney | null + period: { start_date: string; end_date: string } +} + +export interface BrexTitle { + id: string + name: string +} + +export interface BrexListExpensesParams extends BrexPaginationParams { + userIds?: string + statuses?: string + paymentStatuses?: string + purchasedAtStart?: string + purchasedAtEnd?: string +} + +export interface BrexGetExpenseParams { + apiKey: string + expenseId: string +} + +export interface BrexUpdateExpenseParams { + apiKey: string + expenseId: string + memo: string +} + +export interface BrexUploadReceiptParams { + apiKey: string + expenseId: string + file?: unknown + receiptName?: string +} + +export interface BrexMatchReceiptParams { + apiKey: string + file?: unknown + receiptName?: string +} + +export interface BrexListCardTransactionsParams extends BrexPaginationParams { + userIds?: string + postedAtStart?: string +} + +export interface BrexListCashTransactionsParams extends BrexPaginationParams { + accountId: string + postedAtStart?: string +} + +export interface BrexListUsersParams extends BrexPaginationParams { + email?: string +} + +export interface BrexGetUserParams { + apiKey: string + userId: string +} + +export interface BrexNameFilterParams extends BrexPaginationParams { + name?: string +} + +export interface BrexListSpendLimitsParams extends BrexPaginationParams { + memberUserIds?: string +} + +export interface BrexApiKeyParams { + apiKey: string +} + +export interface BrexGetCashAccountParams { + apiKey: string + accountId?: string +} + +export interface BrexListCardsParams extends BrexPaginationParams { + userId?: string +} + +export interface BrexListCashStatementsParams extends BrexPaginationParams { + accountId: string +} + +export interface BrexGetBudgetParams { + apiKey: string + budgetId: string +} + +export interface BrexGetSpendLimitParams { + apiKey: string + spendLimitId: string +} + +export interface BrexGetVendorParams { + apiKey: string + vendorId: string +} + +export interface BrexGetTransferParams { + apiKey: string + transferId: string +} + +export interface BrexListExpensesResponse extends ToolResponse { + output: { + items: BrexExpense[] + nextCursor: string | null + } +} + +export interface BrexGetExpenseResponse extends ToolResponse { + output: { + id: string + memo: string | null + status: string | null + paymentStatus: string | null + expenseType: string | null + category: string | null + merchantId: string | null + merchant: BrexExpense['merchant'] + budgetId: string | null + budget: BrexExpense['budget'] + departmentId: string | null + department: BrexExpense['department'] + locationId: string | null + location: BrexExpense['location'] + userId: string | null + user: BrexExpense['user'] + originalAmount: BrexMoney | null + billingAmount: BrexMoney | null + purchasedAmount: BrexMoney | null + usdEquivalentAmount: BrexMoney | null + purchasedAt: string | null + updatedAt: string + paymentPostedAt: string | null + receipts: BrexExpenseReceipt[] + dashboardUrl: string + } +} + +export interface BrexUpdateExpenseResponse extends ToolResponse { + output: { + id: string + memo: string | null + status: string | null + paymentStatus: string | null + category: string | null + merchantId: string | null + budgetId: string | null + originalAmount: BrexMoney | null + billingAmount: BrexMoney | null + purchasedAt: string | null + updatedAt: string + } +} + +export interface BrexUploadReceiptResponse extends ToolResponse { + output: { + receiptId: string + receiptName: string + expenseId: string | null + } +} + +export interface BrexListCardTransactionsResponse extends ToolResponse { + output: { + items: BrexCardTransaction[] + nextCursor: string | null + } +} + +export interface BrexListCashTransactionsResponse extends ToolResponse { + output: { + items: BrexCashTransaction[] + nextCursor: string | null + } +} + +export interface BrexListCardAccountsResponse extends ToolResponse { + output: { + accounts: BrexCardAccount[] + } +} + +export interface BrexListCashAccountsResponse extends ToolResponse { + output: { + items: BrexCashAccount[] + nextCursor: string | null + } +} + +export interface BrexListUsersResponse extends ToolResponse { + output: { + items: BrexUser[] + nextCursor: string | null + } +} + +export interface BrexGetUserResponse extends ToolResponse { + output: { + id: string + firstName: string + lastName: string + email: string + status: string | null + managerId: string | null + departmentId: string | null + locationId: string | null + titleId: string | null + } +} + +export interface BrexListDepartmentsResponse extends ToolResponse { + output: { + items: BrexDepartment[] + nextCursor: string | null + } +} + +export interface BrexListLocationsResponse extends ToolResponse { + output: { + items: BrexLocation[] + nextCursor: string | null + } +} + +export interface BrexListBudgetsResponse extends ToolResponse { + output: { + items: BrexBudget[] + nextCursor: string | null + } +} + +export interface BrexListSpendLimitsResponse extends ToolResponse { + output: { + items: BrexSpendLimit[] + nextCursor: string | null + } +} + +export interface BrexListVendorsResponse extends ToolResponse { + output: { + items: BrexVendor[] + nextCursor: string | null + } +} + +export interface BrexListTransfersResponse extends ToolResponse { + output: { + items: BrexTransfer[] + nextCursor: string | null + } +} + +export interface BrexGetCompanyResponse extends ToolResponse { + output: { + id: string + legalName: string + mailingAddress: Record | null + accountType: string | null + } +} + +export interface BrexListCardsResponse extends ToolResponse { + output: { + items: BrexCard[] + nextCursor: string | null + } +} + +export interface BrexListTitlesResponse extends ToolResponse { + output: { + items: BrexTitle[] + nextCursor: string | null + } +} + +export interface BrexGetCashAccountResponse extends ToolResponse { + output: { + id: string + name: string + status: string | null + currentBalance: BrexMoney | null + availableBalance: BrexMoney | null + accountNumber: string + routingNumber: string + primary: boolean + } +} + +export interface BrexListStatementsResponse extends ToolResponse { + output: { + items: BrexStatement[] + nextCursor: string | null + } +} + +export interface BrexGetBudgetResponse extends ToolResponse { + output: { + budgetId: string + accountId: string + name: string + description: string | null + parentBudgetId: string | null + ownerUserIds: string[] + periodRecurrenceType: string + startDate: string | null + endDate: string | null + amount: BrexMoney | null + spendBudgetStatus: string + limitType: string | null + } +} + +export interface BrexGetSpendLimitResponse extends ToolResponse { + output: { + id: string + accountId: string + name: string + description: string | null + parentBudgetId: string | null + status: string + periodRecurrenceType: string + spendType: string + startDate: string | null + endDate: string | null + ownerUserIds: string[] + memberUserIds: string[] + currentPeriodBalance: BrexMoney | null + authorizationSettings: Record | null + } +} + +export interface BrexGetVendorResponse extends ToolResponse { + output: { + id: string + companyName: string | null + email: string | null + phone: string | null + paymentAccounts: unknown[] + } +} + +export interface BrexGetTransferResponse extends ToolResponse { + output: { + id: string + counterparty: Record | null + description: string | null + paymentType: string + amount: BrexMoney | null + processDate: string | null + originatingAccount: Record | null + status: string + cancellationReason: string | null + estimatedDeliveryDate: string | null + creatorUserId: string | null + createdAt: string | null + displayName: string | null + externalMemo: string | null + } +} + +export type BrexResponse = + | BrexListExpensesResponse + | BrexGetExpenseResponse + | BrexUpdateExpenseResponse + | BrexUploadReceiptResponse + | BrexListCardTransactionsResponse + | BrexListCashTransactionsResponse + | BrexListCardAccountsResponse + | BrexListCashAccountsResponse + | BrexListUsersResponse + | BrexGetUserResponse + | BrexListDepartmentsResponse + | BrexListLocationsResponse + | BrexListBudgetsResponse + | BrexListSpendLimitsResponse + | BrexListVendorsResponse + | BrexListTransfersResponse + | BrexGetCompanyResponse + | BrexListCardsResponse + | BrexListTitlesResponse + | BrexGetCashAccountResponse + | BrexListStatementsResponse + | BrexGetBudgetResponse + | BrexGetSpendLimitResponse + | BrexGetVendorResponse + | BrexGetTransferResponse + +export const BREX_MONEY_PROPERTIES: Record = { + amount: { + type: 'number', + description: 'Amount in the smallest unit of the currency (e.g., cents for USD)', + }, + currency: { + type: 'string', + description: 'ISO 4217 currency code (e.g., USD)', + optional: true, + }, +} + +export const BREX_EXPENSE_ITEM_PROPERTIES: Record = { + id: { type: 'string', description: 'Unique expense ID' }, + memo: { type: 'string', description: 'Memo on the expense', optional: true }, + status: { + type: 'string', + description: + 'Expense status (DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED)', + optional: true, + }, + payment_status: { + type: 'string', + description: + 'Payment status (NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED)', + optional: true, + }, + expense_type: { + type: 'string', + description: 'Expense type (CARD, BILLPAY, REIMBURSEMENT, CLAWBACK, UNSET)', + optional: true, + }, + category: { type: 'string', description: 'Merchant category of the expense', optional: true }, + merchant: { + type: 'json', + description: 'Merchant details', + optional: true, + properties: { + raw_descriptor: { type: 'string', description: 'Raw merchant descriptor' }, + mcc: { type: 'string', description: 'Merchant category code' }, + country: { type: 'string', description: 'Merchant country' }, + }, + }, + user: { + type: 'json', + description: 'User who made the expense', + optional: true, + properties: { + id: { type: 'string', description: 'User ID' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + }, + }, + budget: { + type: 'json', + description: 'Budget the expense belongs to', + optional: true, + properties: { + id: { type: 'string', description: 'Budget ID' }, + name: { type: 'string', description: 'Budget name' }, + }, + }, + department: { + type: 'json', + description: 'Department of the expense owner', + optional: true, + properties: { + id: { type: 'string', description: 'Department ID' }, + name: { type: 'string', description: 'Department name' }, + }, + }, + location: { + type: 'json', + description: 'Location of the expense owner', + optional: true, + properties: { + id: { type: 'string', description: 'Location ID' }, + name: { type: 'string', description: 'Location name' }, + }, + }, + original_amount: { + type: 'json', + description: 'Original transaction amount', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + billing_amount: { + type: 'json', + description: 'Amount billed to the account', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + purchased_amount: { + type: 'json', + description: 'Amount at the time of purchase', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + receipts: { + type: 'array', + description: 'Receipts attached to the expense', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Receipt ID' }, + download_uris: { type: 'array', description: 'Pre-signed receipt download URLs' }, + }, + }, + }, + purchased_at: { type: 'string', description: 'Purchase timestamp (ISO 8601)', optional: true }, + updated_at: { type: 'string', description: 'Last update timestamp (ISO 8601)' }, + dashboard_url: { type: 'string', description: 'Link to the expense in the Brex dashboard' }, +} + +export const BREX_CARD_TRANSACTION_PROPERTIES: Record = { + id: { type: 'string', description: 'Unique transaction ID' }, + card_id: { type: 'string', description: 'ID of the card used', optional: true }, + description: { type: 'string', description: 'Transaction description' }, + amount: { + type: 'json', + description: 'Transaction amount', + properties: BREX_MONEY_PROPERTIES, + }, + initiated_at_date: { type: 'string', description: 'Date the transaction was initiated' }, + posted_at_date: { type: 'string', description: 'Date the transaction was posted' }, + type: { + type: 'string', + description: + 'Transaction type (PURCHASE, REFUND, CHARGEBACK, REWARDS_CREDIT, COLLECTION, BNPL_FEE)', + optional: true, + }, + merchant: { + type: 'json', + description: 'Merchant details', + optional: true, + properties: { + raw_descriptor: { type: 'string', description: 'Raw merchant descriptor' }, + mcc: { type: 'string', description: 'Merchant category code' }, + country: { type: 'string', description: 'Merchant country' }, + }, + }, + expense_id: { type: 'string', description: 'Associated expense ID', optional: true }, +} + +export const BREX_CASH_TRANSACTION_PROPERTIES: Record = { + id: { type: 'string', description: 'Unique transaction ID' }, + description: { type: 'string', description: 'Transaction description' }, + amount: { + type: 'json', + description: 'Transaction amount', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + initiated_at_date: { type: 'string', description: 'Date the transaction was initiated' }, + posted_at_date: { type: 'string', description: 'Date the transaction was posted' }, + type: { type: 'string', description: 'Transaction type', optional: true }, + transfer_id: { type: 'string', description: 'Associated transfer ID', optional: true }, +} + +export const BREX_USER_PROPERTIES: Record = { + id: { type: 'string', description: 'Unique user ID' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Email address' }, + status: { + type: 'string', + description: 'User status (e.g., INVITED, ACTIVE, CLOSED, DISABLED)', + optional: true, + }, + manager_id: { type: 'string', description: 'ID of the manager', optional: true }, + department_id: { type: 'string', description: 'Department ID', optional: true }, + location_id: { type: 'string', description: 'Location ID', optional: true }, + title_id: { type: 'string', description: 'Title ID', optional: true }, +} diff --git a/apps/sim/tools/brex/update_expense.ts b/apps/sim/tools/brex/update_expense.ts new file mode 100644 index 0000000000..aa037e9811 --- /dev/null +++ b/apps/sim/tools/brex/update_expense.ts @@ -0,0 +1,95 @@ +import type { BrexUpdateExpenseParams, BrexUpdateExpenseResponse } from '@/tools/brex/types' +import { BREX_MONEY_PROPERTIES } from '@/tools/brex/types' +import { BREX_API_BASE, buildBrexHeaders, parseBrexJson } from '@/tools/brex/utils' +import type { ToolConfig } from '@/tools/types' + +export const brexUpdateExpenseTool: ToolConfig = + { + id: 'brex_update_expense', + name: 'Brex Update Expense', + description: 'Update the memo of a Brex card expense', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + expenseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the card expense to update', + }, + memo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New memo for the expense', + }, + }, + + request: { + url: (params) => + `${BREX_API_BASE}/v1/expenses/card/${encodeURIComponent(params.expenseId.trim())}`, + method: 'PUT', + headers: (params) => buildBrexHeaders(params.apiKey), + body: (params) => ({ memo: params.memo }), + }, + + transformResponse: async (response) => { + const data = await parseBrexJson(response) + return { + success: true, + output: { + id: data.id ?? '', + memo: data.memo ?? null, + status: data.status ?? null, + paymentStatus: data.payment_status ?? null, + category: data.category ?? null, + merchantId: data.merchant_id ?? null, + budgetId: data.budget_id ?? null, + originalAmount: data.original_amount ?? null, + billingAmount: data.billing_amount ?? null, + purchasedAt: data.purchased_at ?? null, + updatedAt: data.updated_at ?? '', + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique expense ID' }, + memo: { type: 'string', description: 'Updated memo on the expense', optional: true }, + status: { + type: 'string', + description: + 'Expense status (DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED)', + optional: true, + }, + paymentStatus: { + type: 'string', + description: + 'Payment status (NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED)', + optional: true, + }, + category: { type: 'string', description: 'Merchant category of the expense', optional: true }, + merchantId: { type: 'string', description: 'Merchant ID', optional: true }, + budgetId: { type: 'string', description: 'Budget ID', optional: true }, + originalAmount: { + type: 'json', + description: 'Original transaction amount', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + billingAmount: { + type: 'json', + description: 'Amount billed to the account', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + purchasedAt: { type: 'string', description: 'Purchase timestamp (ISO 8601)', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp (ISO 8601)' }, + }, + } diff --git a/apps/sim/tools/brex/upload_receipt.ts b/apps/sim/tools/brex/upload_receipt.ts new file mode 100644 index 0000000000..6196a4dc58 --- /dev/null +++ b/apps/sim/tools/brex/upload_receipt.ts @@ -0,0 +1,70 @@ +import type { BrexUploadReceiptParams, BrexUploadReceiptResponse } from '@/tools/brex/types' +import type { ToolConfig } from '@/tools/types' + +export const brexUploadReceiptTool: ToolConfig = + { + id: 'brex_upload_receipt', + name: 'Brex Upload Receipt', + description: 'Upload a receipt file and attach it to a specific Brex card expense', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Brex user token (generated from Developer Settings in the Brex dashboard)', + }, + expenseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the card expense to attach the receipt to', + }, + file: { + type: 'file', + required: true, + visibility: 'user-or-llm', + description: 'Receipt file to upload (max 50 MB)', + }, + receiptName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Receipt file name including extension (defaults to the uploaded file name)', + }, + }, + + request: { + url: '/api/tools/brex/upload-receipt', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + apiKey: params.apiKey, + expenseId: params.expenseId, + file: params.file, + receiptName: params.receiptName, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!data.success) { + throw new Error(data.error || 'Failed to upload receipt') + } + return { + success: true, + output: data.output, + } + }, + + outputs: { + receiptId: { type: 'string', description: 'Unique identifier of the receipt upload' }, + receiptName: { type: 'string', description: 'Name the receipt was uploaded with' }, + expenseId: { + type: 'string', + description: 'ID of the expense the receipt was attached to', + optional: true, + }, + }, + } diff --git a/apps/sim/tools/brex/utils.ts b/apps/sim/tools/brex/utils.ts new file mode 100644 index 0000000000..b4f2fdd2f4 --- /dev/null +++ b/apps/sim/tools/brex/utils.ts @@ -0,0 +1,52 @@ +export const BREX_API_BASE = 'https://api.brex.com' + +/** + * Builds the standard headers for Brex API requests. + */ +export function buildBrexHeaders(apiKey: string): Record { + return { + Authorization: `Bearer ${apiKey}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + } +} + +/** + * Parses a Brex API response body, throwing a descriptive error for non-2xx responses. + */ +export async function parseBrexJson(response: Response) { + if (!response.ok) { + const text = await response.text() + let message = text + try { + const parsed = JSON.parse(text) + message = parsed.message ?? text + } catch { + message = text + } + throw new Error(`Brex API error (${response.status}): ${message}`) + } + return response.json() +} + +/** + * Appends a comma-separated value as repeated query parameters (Brex array syntax). + */ +export function appendBrexArrayParam(query: URLSearchParams, key: string, value?: string): void { + if (!value) return + for (const item of value.split(',')) { + const trimmed = item.trim() + if (trimmed) query.append(key, trimmed) + } +} + +/** + * Appends standard cursor/limit pagination parameters to a query. + */ +export function appendBrexPagination( + query: URLSearchParams, + params: { cursor?: string; limit?: string } +): void { + if (params.cursor) query.append('cursor', params.cursor) + if (params.limit) query.append('limit', params.limit) +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 2923847b02..91dcaf954f 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -312,6 +312,36 @@ import { boxSignResendRequestTool, } from '@/tools/box_sign' import { brandfetchGetBrandTool, brandfetchSearchTool } from '@/tools/brandfetch' +import { + brexGetBudgetTool, + brexGetCashAccountTool, + brexGetCompanyTool, + brexGetCurrentUserTool, + brexGetExpenseTool, + brexGetSpendLimitTool, + brexGetTransferTool, + brexGetUserTool, + brexGetVendorTool, + brexListBudgetsTool, + brexListCardAccountsTool, + brexListCardStatementsTool, + brexListCardsTool, + brexListCardTransactionsTool, + brexListCashAccountsTool, + brexListCashStatementsTool, + brexListCashTransactionsTool, + brexListDepartmentsTool, + brexListExpensesTool, + brexListLocationsTool, + brexListSpendLimitsTool, + brexListTitlesTool, + brexListTransfersTool, + brexListUsersTool, + brexListVendorsTool, + brexMatchReceiptTool, + brexUpdateExpenseTool, + brexUploadReceiptTool, +} from '@/tools/brex' import { brightDataCancelSnapshotTool, brightDataDiscoverTool, @@ -3669,6 +3699,34 @@ export const tools: Record = { athena_stop_query: athenaStopQueryTool, brandfetch_get_brand: brandfetchGetBrandTool, brandfetch_search: brandfetchSearchTool, + brex_get_budget: brexGetBudgetTool, + brex_get_cash_account: brexGetCashAccountTool, + brex_get_company: brexGetCompanyTool, + brex_get_current_user: brexGetCurrentUserTool, + brex_get_expense: brexGetExpenseTool, + brex_get_spend_limit: brexGetSpendLimitTool, + brex_get_transfer: brexGetTransferTool, + brex_get_user: brexGetUserTool, + brex_get_vendor: brexGetVendorTool, + brex_list_budgets: brexListBudgetsTool, + brex_list_card_accounts: brexListCardAccountsTool, + brex_list_card_statements: brexListCardStatementsTool, + brex_list_card_transactions: brexListCardTransactionsTool, + brex_list_cards: brexListCardsTool, + brex_list_cash_accounts: brexListCashAccountsTool, + brex_list_cash_statements: brexListCashStatementsTool, + brex_list_cash_transactions: brexListCashTransactionsTool, + brex_list_departments: brexListDepartmentsTool, + brex_list_expenses: brexListExpensesTool, + brex_list_locations: brexListLocationsTool, + brex_list_spend_limits: brexListSpendLimitsTool, + brex_list_titles: brexListTitlesTool, + brex_list_transfers: brexListTransfersTool, + brex_list_users: brexListUsersTool, + brex_list_vendors: brexListVendorsTool, + brex_match_receipt: brexMatchReceiptTool, + brex_update_expense: brexUpdateExpenseTool, + brex_upload_receipt: brexUploadReceiptTool, brightdata_cancel_snapshot: brightDataCancelSnapshotTool, brightdata_discover: brightDataDiscoverTool, brightdata_download_snapshot: brightDataDownloadSnapshotTool, From 7ab893e31d37449a3dacf728ffbd0c5155004b14 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 16:49:30 -0700 Subject: [PATCH 2/7] fix(brex): reject whitespace-only expense IDs in receipt upload instead of silently falling back to receipt match --- .../tools/brex/upload-receipt/route.test.ts | 23 +++++++++++++++++++ .../api/tools/brex/upload-receipt/route.ts | 9 ++++---- apps/sim/lib/api/contracts/tools/brex.ts | 2 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts b/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts index 9791a1fd6a..a0e28627c9 100644 --- a/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts +++ b/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts @@ -94,6 +94,29 @@ describe('POST /api/tools/brex/upload-receipt', () => { expect(uploadInit.method).toBe('PUT') }) + it('rejects a whitespace-only expense ID instead of falling back to receipt match', async () => { + const response = await POST(createMockRequest('POST', { ...baseBody, expenseId: ' ' })) + expect(response.status).toBe(400) + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('trims a padded expense ID before building the upload URL', async () => { + mockFetch + .mockResolvedValueOnce( + jsonResponse({ id: 'receipt_5', uri: 'https://s3.example.com/presigned' }) + ) + .mockResolvedValueOnce(jsonResponse({})) + + const response = await POST( + createMockRequest('POST', { ...baseBody, expenseId: ' expense_123 ' }) + ) + expect(response.status).toBe(200) + const [createUrl] = mockFetch.mock.calls[0] + expect(createUrl).toBe('https://api.brex.com/v1/expenses/card/expense_123/receipt_upload') + const data = await response.json() + expect(data.output.expenseId).toBe('expense_123') + }) + it('uses receipt match when no expense ID is provided', async () => { mockFetch .mockResolvedValueOnce( diff --git a/apps/sim/app/api/tools/brex/upload-receipt/route.ts b/apps/sim/app/api/tools/brex/upload-receipt/route.ts index f8a9f8e6c5..a5d1098190 100644 --- a/apps/sim/app/api/tools/brex/upload-receipt/route.ts +++ b/apps/sim/app/api/tools/brex/upload-receipt/route.ts @@ -53,13 +53,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } const effectiveReceiptName = receiptName?.trim() || userFile.name - const trimmedExpenseId = expenseId?.trim() || undefined - const endpoint = trimmedExpenseId - ? `${BREX_API_BASE}/v1/expenses/card/${encodeURIComponent(trimmedExpenseId)}/receipt_upload` + const endpoint = expenseId + ? `${BREX_API_BASE}/v1/expenses/card/${encodeURIComponent(expenseId)}/receipt_upload` : `${BREX_API_BASE}/v1/expenses/card/receipt_match` logger.info( - `[${requestId}] Creating Brex ${trimmedExpenseId ? 'receipt upload' : 'receipt match'}: ${effectiveReceiptName} (${fileBuffer.length} bytes)` + `[${requestId}] Creating Brex ${expenseId ? 'receipt upload' : 'receipt match'}: ${effectiveReceiptName} (${fileBuffer.length} bytes)` ) const createResponse = await fetch(endpoint, { @@ -116,7 +115,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { output: { receiptId: createData.id, receiptName: effectiveReceiptName, - expenseId: trimmedExpenseId ?? null, + expenseId: expenseId ?? null, }, }) } catch (error) { diff --git a/apps/sim/lib/api/contracts/tools/brex.ts b/apps/sim/lib/api/contracts/tools/brex.ts index 9f75ecbc74..6993dcada8 100644 --- a/apps/sim/lib/api/contracts/tools/brex.ts +++ b/apps/sim/lib/api/contracts/tools/brex.ts @@ -5,7 +5,7 @@ import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' export const brexUploadReceiptBodySchema = z.object({ apiKey: z.string().min(1, 'API key is required'), - expenseId: z.string().min(1, 'Expense ID cannot be empty').optional(), + expenseId: z.string().trim().min(1, 'Expense ID cannot be empty').optional(), file: RawFileInputSchema, receiptName: z .string() From 0446c751008920fdb6bdca2df89af4676d51ad67 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 16:51:31 -0700 Subject: [PATCH 3/7] fix(brex): trim receipt name in contract so whitespace-only overrides are rejected --- apps/sim/app/api/tools/brex/upload-receipt/route.test.ts | 6 ++++++ apps/sim/app/api/tools/brex/upload-receipt/route.ts | 2 +- apps/sim/lib/api/contracts/tools/brex.ts | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts b/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts index a0e28627c9..0b264d9366 100644 --- a/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts +++ b/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts @@ -117,6 +117,12 @@ describe('POST /api/tools/brex/upload-receipt', () => { expect(data.output.expenseId).toBe('expense_123') }) + it('rejects a whitespace-only receipt name', async () => { + const response = await POST(createMockRequest('POST', { ...baseBody, receiptName: ' ' })) + expect(response.status).toBe(400) + expect(mockFetch).not.toHaveBeenCalled() + }) + it('uses receipt match when no expense ID is provided', async () => { mockFetch .mockResolvedValueOnce( diff --git a/apps/sim/app/api/tools/brex/upload-receipt/route.ts b/apps/sim/app/api/tools/brex/upload-receipt/route.ts index a5d1098190..94eb13acd8 100644 --- a/apps/sim/app/api/tools/brex/upload-receipt/route.ts +++ b/apps/sim/app/api/tools/brex/upload-receipt/route.ts @@ -52,7 +52,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } - const effectiveReceiptName = receiptName?.trim() || userFile.name + const effectiveReceiptName = receiptName || userFile.name const endpoint = expenseId ? `${BREX_API_BASE}/v1/expenses/card/${encodeURIComponent(expenseId)}/receipt_upload` : `${BREX_API_BASE}/v1/expenses/card/receipt_match` diff --git a/apps/sim/lib/api/contracts/tools/brex.ts b/apps/sim/lib/api/contracts/tools/brex.ts index 6993dcada8..16f541fe3a 100644 --- a/apps/sim/lib/api/contracts/tools/brex.ts +++ b/apps/sim/lib/api/contracts/tools/brex.ts @@ -9,6 +9,7 @@ export const brexUploadReceiptBodySchema = z.object({ file: RawFileInputSchema, receiptName: z .string() + .trim() .min(1, 'Receipt name cannot be empty') .max(255, 'Receipt name must be at most 255 characters') .optional(), From 27ced4368c69665283b85318777cb8262221c0be Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 17:14:18 -0700 Subject: [PATCH 4/7] fix(brex): align spend limit balance shape, enum descriptions, and pagination metadata with Brex API specs --- .../content/docs/en/integrations/brex.mdx | 54 +++++++++++------ apps/sim/blocks/blocks/brex.ts | 25 +++++++- apps/sim/lib/api/contracts/tools/brex.ts | 9 ++- apps/sim/lib/api/contracts/tools/index.ts | 1 + apps/sim/tools/brex/get_cash_account.ts | 6 +- apps/sim/tools/brex/get_current_user.ts | 3 +- apps/sim/tools/brex/get_expense.ts | 7 ++- apps/sim/tools/brex/get_spend_limit.ts | 6 +- apps/sim/tools/brex/get_transfer.ts | 3 +- apps/sim/tools/brex/get_user.ts | 3 +- apps/sim/tools/brex/list_budgets.ts | 2 +- apps/sim/tools/brex/list_cash_accounts.ts | 2 - apps/sim/tools/brex/list_expenses.ts | 4 +- apps/sim/tools/brex/list_spend_limits.ts | 8 +-- apps/sim/tools/brex/list_transfers.ts | 6 +- apps/sim/tools/brex/types.ts | 58 ++++++++++++++++--- apps/sim/tools/brex/update_expense.ts | 7 ++- 17 files changed, 150 insertions(+), 54 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/brex.mdx b/apps/docs/content/docs/en/integrations/brex.mdx index 54d4ab6d14..09b49faf55 100644 --- a/apps/docs/content/docs/en/integrations/brex.mdx +++ b/apps/docs/content/docs/en/integrations/brex.mdx @@ -47,9 +47,9 @@ List expenses in the Brex account with optional filters for user, status, paymen | `statuses` | string | No | Comma-separated expense statuses to filter by: DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED | | `paymentStatuses` | string | No | Comma-separated payment statuses to filter by: NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED | | `purchasedAtStart` | string | No | Only include expenses purchased at or after this ISO 8601 timestamp | -| `purchasedAtEnd` | string | No | Only include expenses purchased before this ISO 8601 timestamp | +| `purchasedAtEnd` | string | No | Only include expenses purchased at or before this ISO 8601 timestamp | | `cursor` | string | No | Pagination cursor from a previous response | -| `limit` | string | No | Number of expenses to return \(default 100, max 1000\) | +| `limit` | string | No | Number of expenses to return \(max 100\) | #### Output @@ -61,7 +61,7 @@ List expenses in the Brex account with optional filters for user, status, paymen | ↳ `status` | string | Expense status \(DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED\) | | ↳ `payment_status` | string | Payment status \(NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED\) | | ↳ `expense_type` | string | Expense type \(CARD, BILLPAY, REIMBURSEMENT, CLAWBACK, UNSET\) | -| ↳ `category` | string | Merchant category of the expense | +| ↳ `category` | string | Expense category \(e.g., RESTAURANTS, RECURRING_SOFTWARE_AND_SAAS, AIRLINE_EXPENSES\) | | ↳ `merchant` | json | Merchant details | | ↳ `raw_descriptor` | string | Raw merchant descriptor | | ↳ `mcc` | string | Merchant category code | @@ -116,7 +116,7 @@ Get a single Brex expense by its ID, including merchant, user, and receipt detai | `status` | string | Expense status \(DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED\) | | `paymentStatus` | string | Payment status \(NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED\) | | `expenseType` | string | Expense type \(CARD, BILLPAY, REIMBURSEMENT, CLAWBACK, UNSET\) | -| `category` | string | Merchant category of the expense | +| `category` | string | Expense category \(e.g., RESTAURANTS, RECURRING_SOFTWARE_AND_SAAS, AIRLINE_EXPENSES\) | | `merchantId` | string | Merchant ID | | `merchant` | json | Merchant details \(raw descriptor, MCC, country\) | | ↳ `raw_descriptor` | string | Raw merchant descriptor | @@ -179,7 +179,7 @@ Update the memo of a Brex card expense | `memo` | string | Updated memo on the expense | | `status` | string | Expense status \(DRAFT, SUBMITTED, APPROVED, OUT_OF_POLICY, VOID, CANCELED, SPLIT, SETTLED\) | | `paymentStatus` | string | Payment status \(NOT_STARTED, PROCESSING, CANCELED, DECLINED, CLEARED, REFUNDING, REFUNDED, CASH_ADVANCE, CREDITED, AWAITING_PAYMENT, SCHEDULED\) | -| `category` | string | Merchant category of the expense | +| `category` | string | Expense category \(e.g., RESTAURANTS, RECURRING_SOFTWARE_AND_SAAS, AIRLINE_EXPENSES\) | | `merchantId` | string | Merchant ID | | `budgetId` | string | Budget ID | | `originalAmount` | json | Original transaction amount | @@ -461,7 +461,7 @@ List users in the Brex account, optionally filtered by email | ↳ `first_name` | string | First name | | ↳ `last_name` | string | Last name | | ↳ `email` | string | Email address | -| ↳ `status` | string | User status \(e.g., INVITED, ACTIVE, CLOSED, DISABLED\) | +| ↳ `status` | string | User status \(INVITED, ACTIVE, CLOSED, DISABLED, DELETED, PENDING_ACTIVATION, INACTIVE, ARCHIVED\) | | ↳ `manager_id` | string | ID of the manager | | ↳ `department_id` | string | Department ID | | ↳ `location_id` | string | Location ID | @@ -487,7 +487,7 @@ Get a Brex user by their ID | `firstName` | string | First name | | `lastName` | string | Last name | | `email` | string | Email address | -| `status` | string | User status \(e.g., INVITED, ACTIVE, CLOSED, DISABLED\) | +| `status` | string | User status \(INVITED, ACTIVE, CLOSED, DISABLED, DELETED, PENDING_ACTIVATION, INACTIVE, ARCHIVED\) | | `managerId` | string | ID of the manager | | `departmentId` | string | Department ID | | `locationId` | string | Location ID | @@ -511,7 +511,7 @@ Get the Brex user associated with the API token | `firstName` | string | First name | | `lastName` | string | Last name | | `email` | string | Email address | -| `status` | string | User status \(e.g., INVITED, ACTIVE, CLOSED, DISABLED\) | +| `status` | string | User status \(INVITED, ACTIVE, CLOSED, DISABLED, DELETED, PENDING_ACTIVATION, INACTIVE, ARCHIVED\) | | `managerId` | string | ID of the manager | | `departmentId` | string | Department ID | | `locationId` | string | Location ID | @@ -658,7 +658,7 @@ List budgets in the Brex account | ↳ `description` | string | Budget description | | ↳ `parent_budget_id` | string | Parent budget ID | | ↳ `owner_user_ids` | array | User IDs of the budget owners | -| ↳ `period_recurrence_type` | string | Budget period recurrence \(e.g., MONTHLY, QUARTERLY, YEARLY, ONE_TIME\) | +| ↳ `period_recurrence_type` | string | Budget period recurrence \(WEEKLY, MONTHLY, QUARTERLY, YEARLY, ONE_TIME\) | | ↳ `start_date` | string | Budget start date | | ↳ `end_date` | string | Budget end date | | ↳ `amount` | json | Budget amount | @@ -722,13 +722,21 @@ List spend limits in the Brex account, optionally filtered by member user | ↳ `description` | string | Spend limit description | | ↳ `parent_budget_id` | string | Parent budget ID | | ↳ `status` | string | Spend limit status | -| ↳ `period_recurrence_type` | string | Period recurrence \(e.g., MONTHLY, QUARTERLY, YEARLY, ONE_TIME\) | +| ↳ `period_recurrence_type` | string | Period recurrence \(PER_WEEK, PER_MONTH, PER_QUARTER, PER_YEAR, ONE_TIME\) | | ↳ `spend_type` | string | Spend type of the limit | | ↳ `owner_user_ids` | array | User IDs of the spend limit owners | | ↳ `member_user_ids` | array | User IDs of the spend limit members | -| ↳ `current_period_balance` | json | Balance for the current period | -| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | -| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `current_period_balance` | json | Spend and rollover amounts for the current period | +| ↳ `start_date` | string | Start date of the current period | +| ↳ `end_date` | string | End date of the current period | +| ↳ `start_time` | string | Start time of the current period \(ISO 8601\) | +| ↳ `end_time` | string | End time of the current period \(ISO 8601\) | +| ↳ `amount_spent` | json | Amount spent in the current period | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `rollover_amount` | json | Amount rolled over from previous periods | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | | `nextCursor` | string | Cursor for fetching the next page of results | ### `brex_get_spend_limit` @@ -758,9 +766,17 @@ Get a Brex spend limit by its ID | `endDate` | string | Spend limit end date | | `ownerUserIds` | array | User IDs of the spend limit owners | | `memberUserIds` | array | User IDs of the spend limit members | -| `currentPeriodBalance` | json | Balance for the current period | -| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | -| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| `currentPeriodBalance` | json | Spend and rollover amounts for the current period | +| ↳ `start_date` | string | Start date of the current period | +| ↳ `end_date` | string | End date of the current period | +| ↳ `start_time` | string | Start time of the current period \(ISO 8601\) | +| ↳ `end_time` | string | End time of the current period \(ISO 8601\) | +| ↳ `amount_spent` | json | Amount spent in the current period | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | +| ↳ `rollover_amount` | json | Amount rolled over from previous periods | +| ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | +| ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | | `authorizationSettings` | json | Authorization settings \(base limit, authorization type, rollover refresh\) | ### `brex_list_vendors` @@ -829,13 +845,13 @@ List money transfers in the Brex account | ↳ `id` | string | Unique transfer ID | | ↳ `counterparty` | json | Transfer counterparty details | | ↳ `description` | string | Transfer description | -| ↳ `payment_type` | string | Payment type \(e.g., ACH, DOMESTIC_WIRE, CHEQUE, INTERNATIONAL_WIRE\) | +| ↳ `payment_type` | string | Payment type \(ACH, DOMESTIC_WIRE, CHEQUE, INTERNATIONAL_WIRE, BOOK_TRANSFER, STABLECOIN\) | | ↳ `amount` | json | Transfer amount | | ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | | ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | | ↳ `process_date` | string | Date the transfer processes | | ↳ `originating_account` | json | Account the transfer originates from | -| ↳ `status` | string | Transfer status \(e.g., SCHEDULED, PROCESSING, COMPLETED, FAILED\) | +| ↳ `status` | string | Transfer status \(PROCESSING, SCHEDULED, PENDING_APPROVAL, FAILED, PROCESSED\) | | ↳ `cancellation_reason` | string | Reason the transfer was canceled | | ↳ `estimated_delivery_date` | string | Estimated delivery date | | ↳ `creator_user_id` | string | ID of the user who created the transfer | @@ -862,7 +878,7 @@ Get a Brex money transfer by its ID | `id` | string | Unique transfer ID | | `counterparty` | json | Transfer counterparty details | | `description` | string | Transfer description | -| `paymentType` | string | Payment type \(ACH, DOMESTIC_WIRE, CHEQUE, INTERNATIONAL_WIRE, BOOK_TRANSFER\) | +| `paymentType` | string | Payment type \(ACH, DOMESTIC_WIRE, CHEQUE, INTERNATIONAL_WIRE, BOOK_TRANSFER, STABLECOIN\) | | `amount` | json | Transfer amount | | ↳ `amount` | number | Amount in the smallest unit of the currency \(e.g., cents for USD\) | | ↳ `currency` | string | ISO 4217 currency code \(e.g., USD\) | diff --git a/apps/sim/blocks/blocks/brex.ts b/apps/sim/blocks/blocks/brex.ts index 43efd6983f..d80251e51d 100644 --- a/apps/sim/blocks/blocks/brex.ts +++ b/apps/sim/blocks/blocks/brex.ts @@ -4,6 +4,24 @@ import { AuthMode, IntegrationType } from '@/blocks/types' import { normalizeFileInput } from '@/blocks/utils' import type { BrexResponse } from '@/tools/brex/types' +const PAGINATED_OPERATIONS = new Set([ + 'list_expenses', + 'list_card_transactions', + 'list_cash_transactions', + 'list_cash_accounts', + 'list_card_statements', + 'list_cash_statements', + 'list_users', + 'list_departments', + 'list_locations', + 'list_titles', + 'list_cards', + 'list_budgets', + 'list_spend_limits', + 'list_vendors', + 'list_transfers', +]) + export const BrexBlock: BlockConfig = { type: 'brex', name: 'Brex', @@ -430,8 +448,10 @@ export const BrexBlock: BlockConfig = { break } - if (params.cursor) result.cursor = String(params.cursor) - if (params.limit) result.limit = String(params.limit) + if (PAGINATED_OPERATIONS.has(operation)) { + if (params.cursor) result.cursor = String(params.cursor) + if (params.limit) result.limit = String(params.limit) + } return result }, @@ -609,6 +629,7 @@ export const BrexBlockMeta = { modules: ['workflows', 'scheduled', 'tables'], category: 'operations', tags: ['automation'], + alsoIntegrations: ['gmail'], }, { icon: BrexIcon, diff --git a/apps/sim/lib/api/contracts/tools/brex.ts b/apps/sim/lib/api/contracts/tools/brex.ts index 16f541fe3a..6e102be19b 100644 --- a/apps/sim/lib/api/contracts/tools/brex.ts +++ b/apps/sim/lib/api/contracts/tools/brex.ts @@ -4,8 +4,13 @@ import { defineRouteContract } from '@/lib/api/contracts/types' import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' export const brexUploadReceiptBodySchema = z.object({ - apiKey: z.string().min(1, 'API key is required'), - expenseId: z.string().trim().min(1, 'Expense ID cannot be empty').optional(), + apiKey: z.string().min(1, 'API key is required').max(512, 'API key is too long'), + expenseId: z + .string() + .trim() + .min(1, 'Expense ID cannot be empty') + .max(255, 'Expense ID must be at most 255 characters') + .optional(), file: RawFileInputSchema, receiptName: z .string() diff --git a/apps/sim/lib/api/contracts/tools/index.ts b/apps/sim/lib/api/contracts/tools/index.ts index b287452fe1..5a09f5095a 100644 --- a/apps/sim/lib/api/contracts/tools/index.ts +++ b/apps/sim/lib/api/contracts/tools/index.ts @@ -1,6 +1,7 @@ export * from './a2a' export * from './agiloft' export * from './asana' +export * from './brex' export * from './communication' export * from './crowdstrike' export * from './cursor' diff --git a/apps/sim/tools/brex/get_cash_account.ts b/apps/sim/tools/brex/get_cash_account.ts index f2f913084a..1063a10236 100644 --- a/apps/sim/tools/brex/get_cash_account.ts +++ b/apps/sim/tools/brex/get_cash_account.ts @@ -46,8 +46,8 @@ export const brexGetCashAccountTool: ToolConfig< id: data.id ?? '', name: data.name ?? '', status: data.status ?? null, - currentBalance: data.current_balance ?? null, - availableBalance: data.available_balance ?? null, + currentBalance: data.current_balance, + availableBalance: data.available_balance, accountNumber: data.account_number ?? '', routingNumber: data.routing_number ?? '', primary: data.primary ?? false, @@ -62,13 +62,11 @@ export const brexGetCashAccountTool: ToolConfig< currentBalance: { type: 'json', description: 'Current balance', - optional: true, properties: BREX_MONEY_PROPERTIES, }, availableBalance: { type: 'json', description: 'Available balance', - optional: true, properties: BREX_MONEY_PROPERTIES, }, accountNumber: { type: 'string', description: 'Bank account number' }, diff --git a/apps/sim/tools/brex/get_current_user.ts b/apps/sim/tools/brex/get_current_user.ts index 73fa32dd1f..169ca3e23b 100644 --- a/apps/sim/tools/brex/get_current_user.ts +++ b/apps/sim/tools/brex/get_current_user.ts @@ -48,7 +48,8 @@ export const brexGetCurrentUserTool: ToolConfig email: { type: 'string', description: 'Email address' }, status: { type: 'string', - description: 'User status (e.g., INVITED, ACTIVE, CLOSED, DISABLED)', + description: + 'User status (INVITED, ACTIVE, CLOSED, DISABLED, DELETED, PENDING_ACTIVATION, INACTIVE, ARCHIVED)', optional: true, }, managerId: { type: 'string', description: 'ID of the manager', optional: true }, diff --git a/apps/sim/tools/brex/list_budgets.ts b/apps/sim/tools/brex/list_budgets.ts index 1da6bf6631..a1d2eab886 100644 --- a/apps/sim/tools/brex/list_budgets.ts +++ b/apps/sim/tools/brex/list_budgets.ts @@ -74,7 +74,7 @@ export const brexListBudgetsTool: ToolConfig | null } } @@ -584,6 +593,33 @@ export const BREX_MONEY_PROPERTIES: Record = { }, } +export const BREX_SPEND_LIMIT_PERIOD_BALANCE_PROPERTIES: Record = { + start_date: { type: 'string', description: 'Start date of the current period', optional: true }, + end_date: { type: 'string', description: 'End date of the current period', optional: true }, + start_time: { + type: 'string', + description: 'Start time of the current period (ISO 8601)', + optional: true, + }, + end_time: { + type: 'string', + description: 'End time of the current period (ISO 8601)', + optional: true, + }, + amount_spent: { + type: 'json', + description: 'Amount spent in the current period', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, + rollover_amount: { + type: 'json', + description: 'Amount rolled over from previous periods', + optional: true, + properties: BREX_MONEY_PROPERTIES, + }, +} + export const BREX_EXPENSE_ITEM_PROPERTIES: Record = { id: { type: 'string', description: 'Unique expense ID' }, memo: { type: 'string', description: 'Memo on the expense', optional: true }, @@ -604,7 +640,12 @@ export const BREX_EXPENSE_ITEM_PROPERTIES: Record = { description: 'Expense type (CARD, BILLPAY, REIMBURSEMENT, CLAWBACK, UNSET)', optional: true, }, - category: { type: 'string', description: 'Merchant category of the expense', optional: true }, + category: { + type: 'string', + description: + 'Expense category (e.g., RESTAURANTS, RECURRING_SOFTWARE_AND_SAAS, AIRLINE_EXPENSES)', + optional: true, + }, merchant: { type: 'json', description: 'Merchant details', @@ -739,7 +780,8 @@ export const BREX_USER_PROPERTIES: Record = { email: { type: 'string', description: 'Email address' }, status: { type: 'string', - description: 'User status (e.g., INVITED, ACTIVE, CLOSED, DISABLED)', + description: + 'User status (INVITED, ACTIVE, CLOSED, DISABLED, DELETED, PENDING_ACTIVATION, INACTIVE, ARCHIVED)', optional: true, }, manager_id: { type: 'string', description: 'ID of the manager', optional: true }, diff --git a/apps/sim/tools/brex/update_expense.ts b/apps/sim/tools/brex/update_expense.ts index aa037e9811..bce7500167 100644 --- a/apps/sim/tools/brex/update_expense.ts +++ b/apps/sim/tools/brex/update_expense.ts @@ -74,7 +74,12 @@ export const brexUpdateExpenseTool: ToolConfig Date: Thu, 11 Jun 2026 17:19:43 -0700 Subject: [PATCH 5/7] improvement(brex): validate pre-signed upload URL with DNS pinning and harden API key input --- .../tools/brex/upload-receipt/route.test.ts | 91 +++++++++++++------ .../api/tools/brex/upload-receipt/route.ts | 27 +++++- apps/sim/lib/api/contracts/tools/brex.ts | 6 +- 3 files changed, 91 insertions(+), 33 deletions(-) diff --git a/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts b/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts index 0b264d9366..4453ddc1df 100644 --- a/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts +++ b/apps/sim/app/api/tools/brex/upload-receipt/route.test.ts @@ -1,7 +1,12 @@ /** * @vitest-environment node */ -import { createMockRequest, hybridAuthMockFns } from '@sim/testing' +import { + createMockRequest, + hybridAuthMockFns, + inputValidationMock, + inputValidationMockFns, +} from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockProcessFilesToUserFiles, mockDownloadFileFromStorage, mockAssertToolFileAccess } = @@ -11,6 +16,7 @@ const { mockProcessFilesToUserFiles, mockDownloadFileFromStorage, mockAssertTool mockAssertToolFileAccess: vi.fn(), })) +vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) vi.mock('@/lib/uploads/utils/file-utils', () => ({ processFilesToUserFiles: mockProcessFilesToUserFiles, })) @@ -25,6 +31,8 @@ import { POST } from '@/app/api/tools/brex/upload-receipt/route' const mockFetch = vi.fn() +const PINNED_IP = '52.216.0.1' + const baseBody = { apiKey: 'bxt_test_token', expenseId: 'expense_123', @@ -48,6 +56,11 @@ beforeEach(() => { userId: 'user-1', authType: 'internal_jwt', }) + inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValue({ + isValid: true, + resolvedIP: PINNED_IP, + }) + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValue(jsonResponse({})) mockProcessFilesToUserFiles.mockReturnValue([ { key: 'uploads/receipt.pdf', name: 'receipt.pdf', size: 5, type: 'application/pdf' }, ]) @@ -68,11 +81,9 @@ describe('POST /api/tools/brex/upload-receipt', () => { }) it('creates a receipt upload for an expense and PUTs the file to the pre-signed URL', async () => { - mockFetch - .mockResolvedValueOnce( - jsonResponse({ id: 'receipt_1', uri: 'https://s3.example.com/presigned' }) - ) - .mockResolvedValueOnce(jsonResponse({})) + mockFetch.mockResolvedValueOnce( + jsonResponse({ id: 'receipt_1', uri: 'https://s3.example.com/presigned' }) + ) const response = await POST(createMockRequest('POST', baseBody)) expect(response.status).toBe(200) @@ -82,15 +93,21 @@ describe('POST /api/tools/brex/upload-receipt', () => { output: { receiptId: 'receipt_1', receiptName: 'receipt.pdf', expenseId: 'expense_123' }, }) - expect(mockFetch).toHaveBeenCalledTimes(2) + expect(mockFetch).toHaveBeenCalledTimes(1) const [createUrl, createInit] = mockFetch.mock.calls[0] expect(createUrl).toBe('https://api.brex.com/v1/expenses/card/expense_123/receipt_upload') expect(createInit.method).toBe('POST') expect(createInit.headers.Authorization).toBe('Bearer bxt_test_token') expect(JSON.parse(createInit.body)).toEqual({ receipt_name: 'receipt.pdf' }) - const [uploadUrl, uploadInit] = mockFetch.mock.calls[1] + expect(inputValidationMockFns.mockValidateUrlWithDNS).toHaveBeenCalledWith( + 'https://s3.example.com/presigned', + 'uri' + ) + const [uploadUrl, pinnedIP, uploadInit] = + inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[0] expect(uploadUrl).toBe('https://s3.example.com/presigned') + expect(pinnedIP).toBe(PINNED_IP) expect(uploadInit.method).toBe('PUT') }) @@ -101,11 +118,9 @@ describe('POST /api/tools/brex/upload-receipt', () => { }) it('trims a padded expense ID before building the upload URL', async () => { - mockFetch - .mockResolvedValueOnce( - jsonResponse({ id: 'receipt_5', uri: 'https://s3.example.com/presigned' }) - ) - .mockResolvedValueOnce(jsonResponse({})) + mockFetch.mockResolvedValueOnce( + jsonResponse({ id: 'receipt_5', uri: 'https://s3.example.com/presigned' }) + ) const response = await POST( createMockRequest('POST', { ...baseBody, expenseId: ' expense_123 ' }) @@ -123,12 +138,18 @@ describe('POST /api/tools/brex/upload-receipt', () => { expect(mockFetch).not.toHaveBeenCalled() }) + it('rejects an API key containing header-breaking characters', async () => { + const response = await POST( + createMockRequest('POST', { ...baseBody, apiKey: 'bxt_test\r\nX-Injected: 1' }) + ) + expect(response.status).toBe(400) + expect(mockFetch).not.toHaveBeenCalled() + }) + it('uses receipt match when no expense ID is provided', async () => { - mockFetch - .mockResolvedValueOnce( - jsonResponse({ id: 'receipt_2', uri: 'https://s3.example.com/presigned' }) - ) - .mockResolvedValueOnce(jsonResponse({})) + mockFetch.mockResolvedValueOnce( + jsonResponse({ id: 'receipt_2', uri: 'https://s3.example.com/presigned' }) + ) const response = await POST( createMockRequest('POST', { apiKey: 'bxt_test_token', file: baseBody.file }) @@ -146,11 +167,9 @@ describe('POST /api/tools/brex/upload-receipt', () => { }) it('honors a receipt name override', async () => { - mockFetch - .mockResolvedValueOnce( - jsonResponse({ id: 'receipt_3', uri: 'https://s3.example.com/presigned' }) - ) - .mockResolvedValueOnce(jsonResponse({})) + mockFetch.mockResolvedValueOnce( + jsonResponse({ id: 'receipt_3', uri: 'https://s3.example.com/presigned' }) + ) const response = await POST( createMockRequest('POST', { ...baseBody, receiptName: 'march-dinner.pdf' }) @@ -169,6 +188,7 @@ describe('POST /api/tools/brex/upload-receipt', () => { expect(data.success).toBe(false) expect(data.error).toContain('Expense not found') expect(mockFetch).toHaveBeenCalledTimes(1) + expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() }) it('rejects files over the 50 MB limit', async () => { @@ -181,12 +201,27 @@ describe('POST /api/tools/brex/upload-receipt', () => { expect(mockFetch).not.toHaveBeenCalled() }) + it('blocks pre-signed URLs that fail SSRF validation', async () => { + mockFetch.mockResolvedValueOnce( + jsonResponse({ id: 'receipt_6', uri: 'https://169.254.169.254/latest/meta-data' }) + ) + inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValueOnce({ + isValid: false, + error: 'uri resolves to a blocked IP address', + }) + + const response = await POST(createMockRequest('POST', baseBody)) + expect(response.status).toBe(502) + const data = await response.json() + expect(data.error).toContain('invalid upload URL') + expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() + }) + it('fails when the pre-signed upload fails', async () => { - mockFetch - .mockResolvedValueOnce( - jsonResponse({ id: 'receipt_4', uri: 'https://s3.example.com/presigned' }) - ) - .mockResolvedValueOnce(jsonResponse({}, 403)) + mockFetch.mockResolvedValueOnce( + jsonResponse({ id: 'receipt_4', uri: 'https://s3.example.com/presigned' }) + ) + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce(jsonResponse({}, 403)) const response = await POST(createMockRequest('POST', baseBody)) expect(response.status).toBe(502) diff --git a/apps/sim/app/api/tools/brex/upload-receipt/route.ts b/apps/sim/app/api/tools/brex/upload-receipt/route.ts index 94eb13acd8..792e9ab6d5 100644 --- a/apps/sim/app/api/tools/brex/upload-receipt/route.ts +++ b/apps/sim/app/api/tools/brex/upload-receipt/route.ts @@ -4,6 +4,10 @@ import { type NextRequest, NextResponse } from 'next/server' import { brexUploadReceiptContract } from '@/lib/api/contracts/tools/brex' import { parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + secureFetchWithPinnedIP, + validateUrlWithDNS, +} from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils' @@ -93,10 +97,25 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } - const uploadResponse = await fetch(createData.uri, { - method: 'PUT', - body: new Uint8Array(fileBuffer), - }) + const uriValidation = await validateUrlWithDNS(createData.uri, 'uri') + if (!uriValidation.isValid) { + logger.error(`[${requestId}] Pre-signed upload URL failed SSRF validation:`, { + error: uriValidation.error, + }) + return NextResponse.json( + { success: false, error: 'Brex returned an invalid upload URL' }, + { status: 502 } + ) + } + + const uploadResponse = await secureFetchWithPinnedIP( + createData.uri, + uriValidation.resolvedIP!, + { + method: 'PUT', + body: new Uint8Array(fileBuffer), + } + ) if (!uploadResponse.ok) { logger.error(`[${requestId}] Receipt upload to pre-signed URL failed:`, { diff --git a/apps/sim/lib/api/contracts/tools/brex.ts b/apps/sim/lib/api/contracts/tools/brex.ts index 6e102be19b..80ae1fccc3 100644 --- a/apps/sim/lib/api/contracts/tools/brex.ts +++ b/apps/sim/lib/api/contracts/tools/brex.ts @@ -4,7 +4,11 @@ import { defineRouteContract } from '@/lib/api/contracts/types' import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' export const brexUploadReceiptBodySchema = z.object({ - apiKey: z.string().min(1, 'API key is required').max(512, 'API key is too long'), + apiKey: z + .string() + .min(1, 'API key is required') + .max(512, 'API key is too long') + .regex(/^[\x21-\x7e]+$/, 'API key contains invalid characters'), expenseId: z .string() .trim() From cfe4ef7b8b5d28f3d09d649d4a4a2e45582e30c7 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 17:22:22 -0700 Subject: [PATCH 6/7] fix(brex): correct shared limit placeholder to reflect the 100-item cap on list expenses --- apps/sim/blocks/blocks/brex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/brex.ts b/apps/sim/blocks/blocks/brex.ts index d80251e51d..08e4648c39 100644 --- a/apps/sim/blocks/blocks/brex.ts +++ b/apps/sim/blocks/blocks/brex.ts @@ -316,7 +316,7 @@ export const BrexBlock: BlockConfig = { id: 'limit', title: 'Limit', type: 'short-input', - placeholder: 'Number of results to return (default 100, max 1000)', + placeholder: 'Number of results to return (default 100; List Expenses caps at 100)', mode: 'advanced', condition: { field: 'operation', From 5be552161172f8ab852f29d54b740f60801e5e8a Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 17:39:27 -0700 Subject: [PATCH 7/7] fix(brex): normalize timezone-suffixed timestamps for transactions date filters (Brex rejects offsets) --- apps/sim/tools/brex/list_card_transactions.ts | 4 +- apps/sim/tools/brex/list_cash_transactions.ts | 4 +- apps/sim/tools/brex/utils.test.ts | 53 +++++++++++++++++++ apps/sim/tools/brex/utils.ts | 13 +++++ 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 apps/sim/tools/brex/utils.test.ts diff --git a/apps/sim/tools/brex/list_card_transactions.ts b/apps/sim/tools/brex/list_card_transactions.ts index 7cb34432b6..d986c9c42e 100644 --- a/apps/sim/tools/brex/list_card_transactions.ts +++ b/apps/sim/tools/brex/list_card_transactions.ts @@ -9,6 +9,7 @@ import { BREX_API_BASE, buildBrexHeaders, parseBrexJson, + toBrexDateTime, } from '@/tools/brex/utils' import type { ToolConfig } from '@/tools/types' @@ -59,7 +60,8 @@ export const brexListCardTransactionsTool: ToolConfig< const query = new URLSearchParams() query.append('expand[]', 'expense_id') appendBrexArrayParam(query, 'user_ids', params.userIds) - if (params.postedAtStart) query.append('posted_at_start', params.postedAtStart) + if (params.postedAtStart) + query.append('posted_at_start', toBrexDateTime(params.postedAtStart)) appendBrexPagination(query, params) return `${BREX_API_BASE}/v2/transactions/card/primary?${query.toString()}` }, diff --git a/apps/sim/tools/brex/list_cash_transactions.ts b/apps/sim/tools/brex/list_cash_transactions.ts index 0d0237e696..5c10209941 100644 --- a/apps/sim/tools/brex/list_cash_transactions.ts +++ b/apps/sim/tools/brex/list_cash_transactions.ts @@ -8,6 +8,7 @@ import { BREX_API_BASE, buildBrexHeaders, parseBrexJson, + toBrexDateTime, } from '@/tools/brex/utils' import type { ToolConfig } from '@/tools/types' @@ -56,7 +57,8 @@ export const brexListCashTransactionsTool: ToolConfig< request: { url: (params) => { const query = new URLSearchParams() - if (params.postedAtStart) query.append('posted_at_start', params.postedAtStart) + if (params.postedAtStart) + query.append('posted_at_start', toBrexDateTime(params.postedAtStart)) appendBrexPagination(query, params) const queryString = query.toString() const base = `${BREX_API_BASE}/v2/transactions/cash/${encodeURIComponent(params.accountId.trim())}` diff --git a/apps/sim/tools/brex/utils.test.ts b/apps/sim/tools/brex/utils.test.ts new file mode 100644 index 0000000000..d7e7b727de --- /dev/null +++ b/apps/sim/tools/brex/utils.test.ts @@ -0,0 +1,53 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { appendBrexArrayParam, appendBrexPagination, toBrexDateTime } from '@/tools/brex/utils' + +describe('toBrexDateTime', () => { + it('strips a Z suffix by converting to naive UTC', () => { + expect(toBrexDateTime('2026-01-01T00:00:00Z')).toBe('2026-01-01T00:00:00') + expect(toBrexDateTime('2026-01-01T12:30:45.123Z')).toBe('2026-01-01T12:30:45') + }) + + it('converts timezone offsets to UTC before stripping', () => { + expect(toBrexDateTime('2026-01-01T02:00:00+02:00')).toBe('2026-01-01T00:00:00') + expect(toBrexDateTime('2025-12-31T19:00:00-05:00')).toBe('2026-01-01T00:00:00') + }) + + it('passes through timestamps without a timezone unchanged', () => { + expect(toBrexDateTime('2026-01-01T00:00:00')).toBe('2026-01-01T00:00:00') + expect(toBrexDateTime('2026-01-01T00:00:00.000')).toBe('2026-01-01T00:00:00.000') + }) + + it('passes through unparseable values unchanged', () => { + expect(toBrexDateTime('not-a-date-Z')).toBe('not-a-date-Z') + }) +}) + +describe('appendBrexArrayParam', () => { + it('appends repeated params from a comma-separated value, trimming entries', () => { + const query = new URLSearchParams() + appendBrexArrayParam(query, 'status[]', ' APPROVED, SETTLED ,, ') + expect(query.getAll('status[]')).toEqual(['APPROVED', 'SETTLED']) + }) + + it('does nothing for an empty value', () => { + const query = new URLSearchParams() + appendBrexArrayParam(query, 'status[]', undefined) + expect(query.toString()).toBe('') + }) +}) + +describe('appendBrexPagination', () => { + it('appends cursor and limit only when present', () => { + const query = new URLSearchParams() + appendBrexPagination(query, { cursor: 'abc', limit: '10' }) + expect(query.get('cursor')).toBe('abc') + expect(query.get('limit')).toBe('10') + + const empty = new URLSearchParams() + appendBrexPagination(empty, {}) + expect(empty.toString()).toBe('') + }) +}) diff --git a/apps/sim/tools/brex/utils.ts b/apps/sim/tools/brex/utils.ts index b4f2fdd2f4..19edd9bb79 100644 --- a/apps/sim/tools/brex/utils.ts +++ b/apps/sim/tools/brex/utils.ts @@ -50,3 +50,16 @@ export function appendBrexPagination( if (params.cursor) query.append('cursor', params.cursor) if (params.limit) query.append('limit', params.limit) } + +/** + * Converts a timestamp to the timezone-less date-time form the Brex Transactions + * API requires (e.g., 2026-01-01T00:00:00). Brex rejects timezone-suffixed + * timestamps on these endpoints, so offsets are converted to UTC and stripped. + */ +export function toBrexDateTime(value: string): string { + const trimmed = value.trim() + if (!/(?:z|[+-]\d{2}:?\d{2})$/i.test(trimmed)) return trimmed + const parsed = new Date(trimmed) + if (Number.isNaN(parsed.getTime())) return trimmed + return parsed.toISOString().slice(0, 19) +}