From 25e04268f3c3ea750c292a84f0b9cb805a829db2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 17:38:44 -0700 Subject: [PATCH 01/13] feat(sap): add SAP Concur integration block and SAP S/4HANA validation fixes --- apps/docs/components/icons.tsx | 19 + apps/docs/components/ui/icon-mapping.ts | 2 + apps/docs/content/docs/en/tools/meta.json | 1 + .../docs/content/docs/en/tools/sap_concur.mdx | 2766 +++++++++++++++++ .../docs/content/docs/en/tools/sap_s4hana.mdx | 1005 ++++-- .../integrations/data/icon-mapping.ts | 2 + .../integrations/data/integrations.json | 315 +- .../app/api/tools/sap_concur/proxy/route.ts | 133 + apps/sim/app/api/tools/sap_concur/shared.ts | 300 ++ .../app/api/tools/sap_concur/upload/route.ts | 279 ++ .../app/api/tools/sap_s4hana/proxy/route.ts | 71 +- apps/sim/blocks/blocks/sap_concur.ts | 1920 ++++++++++++ apps/sim/blocks/blocks/sap_s4hana.ts | 39 +- apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 19 + apps/sim/tools/registry.ts | 142 + .../sap_concur/approve_expense_report.ts | 95 + .../tools/sap_concur/associate_attendees.ts | 123 + .../tools/sap_concur/create_cash_advance.ts | 90 + .../sap_concur/create_expected_expense.ts | 179 ++ .../tools/sap_concur/create_expense_report.ts | 109 + apps/sim/tools/sap_concur/create_list_item.ts | 121 + .../sap_concur/create_purchase_request.ts | 115 + .../tools/sap_concur/create_quick_expense.ts | 110 + .../create_quick_expense_with_image.ts | 123 + .../tools/sap_concur/create_report_comment.ts | 119 + .../tools/sap_concur/create_travel_request.ts | 264 ++ apps/sim/tools/sap_concur/create_user.ts | 85 + .../sap_concur/delete_expected_expense.ts | 100 + apps/sim/tools/sap_concur/delete_expense.ts | 96 + .../tools/sap_concur/delete_expense_report.ts | 86 + apps/sim/tools/sap_concur/delete_list_item.ts | 88 + .../tools/sap_concur/delete_travel_request.ts | 99 + apps/sim/tools/sap_concur/delete_user.ts | 87 + apps/sim/tools/sap_concur/get_allocation.ts | 156 + apps/sim/tools/sap_concur/get_budget.ts | 179 ++ apps/sim/tools/sap_concur/get_cash_advance.ts | 191 ++ .../sim/tools/sap_concur/get_exchange_rate.ts | 103 + .../tools/sap_concur/get_expected_expense.ts | 167 + apps/sim/tools/sap_concur/get_expense.ts | 357 +++ .../tools/sap_concur/get_expense_report.ts | 292 ++ apps/sim/tools/sap_concur/get_itemizations.ts | 174 ++ apps/sim/tools/sap_concur/get_itinerary.ts | 230 ++ apps/sim/tools/sap_concur/get_list.ts | 134 + apps/sim/tools/sap_concur/get_list_item.ts | 123 + .../tools/sap_concur/get_purchase_request.ts | 155 + apps/sim/tools/sap_concur/get_receipt.ts | 120 + .../tools/sap_concur/get_receipt_status.ts | 106 + .../sap_concur/get_request_cash_advance.ts | 130 + .../tools/sap_concur/get_travel_profile.ts | 234 ++ .../tools/sap_concur/get_travel_request.ts | 336 ++ apps/sim/tools/sap_concur/get_user.ts | 105 + apps/sim/tools/sap_concur/index.ts | 70 + .../tools/sap_concur/issue_cash_advance.ts | 109 + apps/sim/tools/sap_concur/list_allocations.ts | 117 + .../sap_concur/list_attendee_associations.ts | 193 ++ .../sap_concur/list_budget_categories.ts | 106 + apps/sim/tools/sap_concur/list_budgets.ts | 112 + apps/sim/tools/sap_concur/list_exceptions.ts | 131 + .../sap_concur/list_expected_expenses.ts | 100 + .../tools/sap_concur/list_expense_reports.ts | 275 ++ apps/sim/tools/sap_concur/list_expenses.ts | 216 ++ apps/sim/tools/sap_concur/list_itineraries.ts | 232 ++ apps/sim/tools/sap_concur/list_list_items.ts | 220 ++ apps/sim/tools/sap_concur/list_lists.ts | 209 ++ apps/sim/tools/sap_concur/list_receipts.ts | 107 + .../tools/sap_concur/list_report_comments.ts | 154 + .../sap_concur/list_reports_to_approve.ts | 175 ++ .../list_travel_profiles_summary.ts | 208 ++ .../list_travel_request_comments.ts | 118 + .../tools/sap_concur/list_travel_requests.ts | 349 +++ apps/sim/tools/sap_concur/list_users.ts | 109 + .../tools/sap_concur/move_travel_request.ts | 151 + .../tools/sap_concur/recall_expense_report.ts | 109 + .../tools/sap_concur/remove_all_attendees.ts | 110 + apps/sim/tools/sap_concur/search_locations.ts | 227 ++ apps/sim/tools/sap_concur/search_users.ts | 87 + .../sap_concur/send_back_expense_report.ts | 95 + .../tools/sap_concur/submit_expense_report.ts | 102 + apps/sim/tools/sap_concur/types.ts | 531 ++++ .../sim/tools/sap_concur/update_allocation.ts | 116 + .../sap_concur/update_expected_expense.ts | 177 ++ apps/sim/tools/sap_concur/update_expense.ts | 104 + .../tools/sap_concur/update_expense_report.ts | 109 + apps/sim/tools/sap_concur/update_list_item.ts | 131 + .../tools/sap_concur/update_travel_request.ts | 231 ++ apps/sim/tools/sap_concur/update_user.ts | 95 + .../tools/sap_concur/upload_receipt_image.ts | 119 + apps/sim/tools/sap_concur/utils.ts | 297 ++ .../sap_s4hana/create_business_partner.ts | 68 +- .../tools/sap_s4hana/create_purchase_order.ts | 47 +- .../sap_s4hana/create_purchase_requisition.ts | 46 +- .../tools/sap_s4hana/create_sales_order.ts | 44 +- .../tools/sap_s4hana/delete_sales_order.ts | 14 +- .../tools/sap_s4hana/get_billing_document.ts | 160 +- .../tools/sap_s4hana/get_business_partner.ts | 80 +- apps/sim/tools/sap_s4hana/get_customer.ts | 47 +- .../tools/sap_s4hana/get_inbound_delivery.ts | 89 +- .../tools/sap_s4hana/get_material_document.ts | 66 +- .../tools/sap_s4hana/get_outbound_delivery.ts | 103 +- apps/sim/tools/sap_s4hana/get_product.ts | 107 +- .../tools/sap_s4hana/get_purchase_order.ts | 84 +- .../sap_s4hana/get_purchase_requisition.ts | 44 +- apps/sim/tools/sap_s4hana/get_sales_order.ts | 80 +- apps/sim/tools/sap_s4hana/get_supplier.ts | 170 +- .../tools/sap_s4hana/get_supplier_invoice.ts | 87 +- .../sap_s4hana/list_billing_documents.ts | 176 +- .../sap_s4hana/list_business_partners.ts | 75 +- apps/sim/tools/sap_s4hana/list_customers.ts | 51 +- .../sap_s4hana/list_inbound_deliveries.ts | 105 +- .../sap_s4hana/list_material_documents.ts | 68 +- .../tools/sap_s4hana/list_material_stock.ts | 56 +- .../sap_s4hana/list_outbound_deliveries.ts | 115 +- apps/sim/tools/sap_s4hana/list_products.ts | 127 +- .../tools/sap_s4hana/list_purchase_orders.ts | 77 +- .../sap_s4hana/list_purchase_requisitions.ts | 53 +- .../sim/tools/sap_s4hana/list_sales_orders.ts | 79 +- .../sap_s4hana/list_supplier_invoices.ts | 103 +- apps/sim/tools/sap_s4hana/list_suppliers.ts | 188 +- apps/sim/tools/sap_s4hana/odata_query.ts | 10 +- .../sap_s4hana/update_business_partner.ts | 49 +- apps/sim/tools/sap_s4hana/update_customer.ts | 30 +- apps/sim/tools/sap_s4hana/update_product.ts | 36 +- .../tools/sap_s4hana/update_purchase_order.ts | 48 +- .../sap_s4hana/update_purchase_requisition.ts | 40 +- .../tools/sap_s4hana/update_sales_order.ts | 48 +- apps/sim/tools/sap_s4hana/update_supplier.ts | 58 +- scripts/check-api-validation-contracts.ts | 4 +- 128 files changed, 20981 insertions(+), 448 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/sap_concur.mdx create mode 100644 apps/sim/app/api/tools/sap_concur/proxy/route.ts create mode 100644 apps/sim/app/api/tools/sap_concur/shared.ts create mode 100644 apps/sim/app/api/tools/sap_concur/upload/route.ts create mode 100644 apps/sim/blocks/blocks/sap_concur.ts create mode 100644 apps/sim/tools/sap_concur/approve_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/associate_attendees.ts create mode 100644 apps/sim/tools/sap_concur/create_cash_advance.ts create mode 100644 apps/sim/tools/sap_concur/create_expected_expense.ts create mode 100644 apps/sim/tools/sap_concur/create_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/create_list_item.ts create mode 100644 apps/sim/tools/sap_concur/create_purchase_request.ts create mode 100644 apps/sim/tools/sap_concur/create_quick_expense.ts create mode 100644 apps/sim/tools/sap_concur/create_quick_expense_with_image.ts create mode 100644 apps/sim/tools/sap_concur/create_report_comment.ts create mode 100644 apps/sim/tools/sap_concur/create_travel_request.ts create mode 100644 apps/sim/tools/sap_concur/create_user.ts create mode 100644 apps/sim/tools/sap_concur/delete_expected_expense.ts create mode 100644 apps/sim/tools/sap_concur/delete_expense.ts create mode 100644 apps/sim/tools/sap_concur/delete_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/delete_list_item.ts create mode 100644 apps/sim/tools/sap_concur/delete_travel_request.ts create mode 100644 apps/sim/tools/sap_concur/delete_user.ts create mode 100644 apps/sim/tools/sap_concur/get_allocation.ts create mode 100644 apps/sim/tools/sap_concur/get_budget.ts create mode 100644 apps/sim/tools/sap_concur/get_cash_advance.ts create mode 100644 apps/sim/tools/sap_concur/get_exchange_rate.ts create mode 100644 apps/sim/tools/sap_concur/get_expected_expense.ts create mode 100644 apps/sim/tools/sap_concur/get_expense.ts create mode 100644 apps/sim/tools/sap_concur/get_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/get_itemizations.ts create mode 100644 apps/sim/tools/sap_concur/get_itinerary.ts create mode 100644 apps/sim/tools/sap_concur/get_list.ts create mode 100644 apps/sim/tools/sap_concur/get_list_item.ts create mode 100644 apps/sim/tools/sap_concur/get_purchase_request.ts create mode 100644 apps/sim/tools/sap_concur/get_receipt.ts create mode 100644 apps/sim/tools/sap_concur/get_receipt_status.ts create mode 100644 apps/sim/tools/sap_concur/get_request_cash_advance.ts create mode 100644 apps/sim/tools/sap_concur/get_travel_profile.ts create mode 100644 apps/sim/tools/sap_concur/get_travel_request.ts create mode 100644 apps/sim/tools/sap_concur/get_user.ts create mode 100644 apps/sim/tools/sap_concur/index.ts create mode 100644 apps/sim/tools/sap_concur/issue_cash_advance.ts create mode 100644 apps/sim/tools/sap_concur/list_allocations.ts create mode 100644 apps/sim/tools/sap_concur/list_attendee_associations.ts create mode 100644 apps/sim/tools/sap_concur/list_budget_categories.ts create mode 100644 apps/sim/tools/sap_concur/list_budgets.ts create mode 100644 apps/sim/tools/sap_concur/list_exceptions.ts create mode 100644 apps/sim/tools/sap_concur/list_expected_expenses.ts create mode 100644 apps/sim/tools/sap_concur/list_expense_reports.ts create mode 100644 apps/sim/tools/sap_concur/list_expenses.ts create mode 100644 apps/sim/tools/sap_concur/list_itineraries.ts create mode 100644 apps/sim/tools/sap_concur/list_list_items.ts create mode 100644 apps/sim/tools/sap_concur/list_lists.ts create mode 100644 apps/sim/tools/sap_concur/list_receipts.ts create mode 100644 apps/sim/tools/sap_concur/list_report_comments.ts create mode 100644 apps/sim/tools/sap_concur/list_reports_to_approve.ts create mode 100644 apps/sim/tools/sap_concur/list_travel_profiles_summary.ts create mode 100644 apps/sim/tools/sap_concur/list_travel_request_comments.ts create mode 100644 apps/sim/tools/sap_concur/list_travel_requests.ts create mode 100644 apps/sim/tools/sap_concur/list_users.ts create mode 100644 apps/sim/tools/sap_concur/move_travel_request.ts create mode 100644 apps/sim/tools/sap_concur/recall_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/remove_all_attendees.ts create mode 100644 apps/sim/tools/sap_concur/search_locations.ts create mode 100644 apps/sim/tools/sap_concur/search_users.ts create mode 100644 apps/sim/tools/sap_concur/send_back_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/submit_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/types.ts create mode 100644 apps/sim/tools/sap_concur/update_allocation.ts create mode 100644 apps/sim/tools/sap_concur/update_expected_expense.ts create mode 100644 apps/sim/tools/sap_concur/update_expense.ts create mode 100644 apps/sim/tools/sap_concur/update_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/update_list_item.ts create mode 100644 apps/sim/tools/sap_concur/update_travel_request.ts create mode 100644 apps/sim/tools/sap_concur/update_user.ts create mode 100644 apps/sim/tools/sap_concur/upload_receipt_image.ts create mode 100644 apps/sim/tools/sap_concur/utils.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index c4bc260742b..4092f8c10a7 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4141,6 +4141,25 @@ export function SapS4HanaIcon(props: SVGProps) { ) } +export function SapConcurIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function ServiceNowIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 48748d79ba4..b515c6ccdd2 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -155,6 +155,7 @@ import { RootlyIcon, S3Icon, SalesforceIcon, + SapConcurIcon, SapS4HanaIcon, SESIcon, SearchIcon, @@ -372,6 +373,7 @@ export const blockTypeToIconMap: Record = { rootly: RootlyIcon, s3: S3Icon, salesforce: SalesforceIcon, + sap_concur: SapConcurIcon, sap_s4hana: SapS4HanaIcon, search: SearchIcon, secrets_manager: SecretsManagerIcon, diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 6beab98ac26..af967f765a0 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -151,6 +151,7 @@ "rootly", "s3", "salesforce", + "sap_concur", "sap_s4hana", "search", "secrets_manager", diff --git a/apps/docs/content/docs/en/tools/sap_concur.mdx b/apps/docs/content/docs/en/tools/sap_concur.mdx new file mode 100644 index 00000000000..9547354bc17 --- /dev/null +++ b/apps/docs/content/docs/en/tools/sap_concur.mdx @@ -0,0 +1,2766 @@ +--- +title: SAP Concur +description: Manage expense reports, travel requests, cash advances, and more in SAP Concur +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[SAP Concur](https://www.concur.com/) is a leading cloud-based platform for travel, expense, and invoice management. It helps organizations capture spend across every channel — corporate cards, receipts, mileage, and travel bookings — and route it through approval, audit, and reimbursement workflows. + +With SAP Concur, you can: + +- **Manage expense reports end-to-end**: Create reports, add expenses and itemizations, allocate costs, attach receipts, and run them through submit/approve/recall/send-back workflows +- **Capture spend at the source**: Upload receipt images, create quick expenses with images, and pull receipts back for matching and audit +- **Automate travel programs**: List and inspect trips and itineraries, manage travel requests with cash advance support, and read traveler profiles +- **Govern users and reference data**: Provision identities via SCIM v4.1, maintain custom lists for cost centers and projects, and look up locations, exchange rates, and budgets +- **Issue and track cash advances**: Create, retrieve, and issue cash advances tied to travel requests or expense reports + +In Sim, the SAP Concur integration empowers your agents to automate AP and travel workflows across every Concur datacenter. Use tool actions to: + +- **Drive expense automation**: Programmatically build, submit, and route expense reports without users opening the Concur UI +- **Sync identities and reference data**: Keep employees, cost centers, and project codes aligned with your HRIS and ERP systems +- **Process receipts at scale**: Forward receipt images from email or Slack into Concur for OCR, matching, and reimbursement +- **Build approval bots**: Surface pending reports to managers, summarize line items, and approve or send back with a single message + +These capabilities let you eliminate manual data entry, accelerate close cycles, and run your travel and expense program as code — all as part of your workflows. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Connect SAP Concur via OAuth 2.0. Manage expense reports and line items, allocations, attendees, comments, exceptions, quick expenses, receipts, travel requests and expected expenses, cash advances, itineraries, user identities, custom lists, budgets, exchange rates, and purchase requests across every Concur datacenter. + + + +## Tools + +### `sap_concur_approve_expense_report` + +Approve an expense report as a manager (PATCH /expensereports/v4/users/{userId}/context/MANAGER/reports/{reportId}/approve). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Manager user UUID approving the report | +| `contextType` | string | Yes | Access context: must be MANAGER | +| `reportId` | string | Yes | Expense report ID to approve | +| `body` | json | Yes | Request body — `comment` is required by Concur \(e.g., \{ "comment": "Approved" \}\). If the report contains rejected expenses, `expenseRejectedComment` is also required. Optional fields: `expectedStepCode`, `expectedStepSequence`, `statusId` \(defaults to "A_APPR"\). | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty \(204 No Content\) | + +### `sap_concur_associate_attendees` + +Associate attendees with an expense (POST /expensereports/v4/users/{userId}/context/TRAVELER/reports/{reportId}/expenses/{expenseId}/attendees). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID | +| `body` | json | Yes | Attendee associations payload \(e.g., \{ "attendeeAssociations": \[...\] \}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Concur association response \(201 Created with URI\) | +| ↳ `uri` | string | Resource URI of the attendee associations collection | + +### `sap_concur_create_cash_advance` + +Create a cash advance (POST /cashadvance/v4.1/cashadvances). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `body` | json | Yes | Cash advance payload | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created cash advance payload | +| ↳ `cashAdvanceId` | string | Unique identifier of the created cash advance | + +### `sap_concur_create_expected_expense` + +Create an expected expense on a travel request (POST /travelrequest/v4/requests/{requestUuid}/expenses). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID | +| `userId` | string | No | User UUID acting on the request \(required when using a Company JWT, optional otherwise\) | +| `body` | json | Yes | Expected expense payload | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created expected expense payload | +| ↳ `id` | string | Expected expense identifier | +| ↳ `href` | string | Self-link to the resource | +| ↳ `expenseType` | json | Expense type \{id, name\} | +| ↳ `transactionDate` | string | Transaction date | +| ↳ `transactionAmount` | json | Transaction amount \{value, currencyCode\} | +| ↳ `postedAmount` | json | Posted amount \{value, currencyCode\} | +| ↳ `approvedAmount` | json | Approved amount \{value, currencyCode\} | +| ↳ `remainingAmount` | json | Remaining amount on the expected expense | +| ↳ `businessPurpose` | string | Business purpose of the expense | +| ↳ `location` | json | Location \{id, name, city, countryCode, countrySubDivisionCode, iataCode, locationType\} | +| ↳ `exchangeRate` | json | Exchange rate \{value, operation\} | +| ↳ `allocations` | json | Budget allocations array \(allocationId, allocationAmount, approvedAmount, postedAmount, expenseId, percentEdited, systemAllocation, percentage\) | +| ↳ `tripData` | json | Trip data \{agencyBooked, selfBooked, tripType \(ONE_WAY\|ROUND_TRIP\), legs\[\{id, returnLeg, startDate, startTime, startLocationDetail, startLocation, endLocation, class \{code,value\}, travelExceptionReasonCodes\}\], segmentType \{category, code\}\} | +| ↳ `parentRequest` | json | Parent travel request resource link \{href, id\} | +| ↳ `comments` | json | Comments sub-resource link \{href, id\} | + +### `sap_concur_create_expense_report` + +Create an expense report (POST /expensereports/v4/users/{userId}/context/{contextType}/reports — supported contexts: TRAVELER, PROXY). Required body fields: name, policyId. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who will own the report | +| `contextType` | string | Yes | Access context: TRAVELER \(creating own report\) or PROXY \(creating on behalf of another user\) | +| `body` | json | Yes | Report payload — `name` and `policyId` are required. Optional fields: businessPurpose, comment, customData, countryCode, countrySubDivisionCode, etc. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created expense report \(Concur returns 201 with a URI to the new report\) | +| ↳ `uri` | string | URI of the newly created expense report | + +### `sap_concur_create_list_item` + +Create a list item (POST /list/v4/items). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `body` | json | Yes | List item payload. Required: listId, shortCode, value. Optional: parentId or parentCode \(mutually exclusive\). Note: Concur rejects shortCode/value containing hyphens. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created list item | +| ↳ `id` | string | List item UUID | +| ↳ `code` | string | Long code format for the item | +| ↳ `shortCode` | string | Short code identifier | +| ↳ `value` | string | Display value of the item | +| ↳ `parentId` | string | Parent item UUID \(omitted for first-level items\) | +| ↳ `level` | number | Hierarchy level \(1 for root items\) | +| ↳ `isDeleted` | boolean | Deletion status across all containing lists | +| ↳ `lists` | array | Lists containing this item | +| ↳ `id` | string | List UUID | +| ↳ `hasChildren` | boolean | Whether this item has children in the list | + +### `sap_concur_create_purchase_request` + +Create a purchase request (POST /purchaserequest/v4/purchaserequests). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `body` | json | Yes | Purchase request payload | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created purchase request payload | +| ↳ `id` | string | Identifier of the created purchase request | +| ↳ `uri` | string | Resource URI for the created purchase request | +| ↳ `errors` | array | Validation or processing errors returned by Concur | +| ↳ `errorCode` | string | Error code | +| ↳ `errorMessage` | string | Error message | +| ↳ `dataPath` | string | Path to the request data which has the error | + +### `sap_concur_create_quick_expense` + +Create a quick expense (POST /quickexpense/v4/users/{userId}/context/TRAVELER/quickexpenses). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who owns the quick expense | +| `contextType` | string | Yes | Access context: must be TRAVELER | +| `body` | json | Yes | Quick expense payload \(expenseTypeId, transactionAmount, transactionDate, etc.\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created quick expense response \(HTTP 201 Created\) | +| ↳ `quickExpenseIdUri` | string | URI of the created quick expense resource | + +### `sap_concur_create_quick_expense_with_image` + +Create a quick expense with an attached image (POST /quickexpense/v4/users/{userId}/context/{contextType}/quickexpenses/image). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: must be TRAVELER | +| `receipt` | json | Yes | Receipt image \(UserFile\). Allowed: PDF, PNG, JPEG, TIFF \(max 50MB\) | +| `body` | json | Yes | Quick expense payload \(transactionAmount, transactionDate, expenseTypeId, vendor, ...\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created quick expense response \(HTTP 201 with attached receipt image\) | +| ↳ `quickExpenseIdUri` | string | URI of the created quick expense resource | + +### `sap_concur_create_report_comment` + +Create a comment on a report (POST /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/comments). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER, MANAGER, or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `comment` | string | Yes | Comment text to add | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created comment response \(Concur returns 201 Created with URI\) | +| ↳ `uri` | string | Resource URI of the created comment | + +### `sap_concur_create_travel_request` + +Create a travel request (POST /travelrequest/v4/requests). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | No | Optional Concur user UUID — required when impersonating another user | +| `body` | json | Yes | Travel request payload \(name, purpose, startDate, endDate, requestPolicyId, etc.\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created travel request payload | +| ↳ `id` | string | Travel request UUID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `requestId` | string | Public-facing request ID \(4-6 alphanumeric characters\) | +| ↳ `name` | string | Request name | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `comment` | string | Last attached comment | +| ↳ `creationDate` | string | Creation timestamp | +| ↳ `lastModified` | string | Last modification timestamp | +| ↳ `submitDate` | string | Last submission timestamp | +| ↳ `startDate` | string | Trip start date \(ISO 8601\) | +| ↳ `endDate` | string | Trip end date \(ISO 8601\) | +| ↳ `startTime` | string | Trip start time \(HH:mm\) | +| ↳ `endTime` | string | Trip end time \(HH:mm\) | +| ↳ `approved` | boolean | Whether the request is approved | +| ↳ `pendingApproval` | boolean | Pending approval flag | +| ↳ `closed` | boolean | Closed flag | +| ↳ `everSentBack` | boolean | Ever-sent-back flag | +| ↳ `canceledPostApproval` | boolean | Canceled after approval flag | +| ↳ `approvalStatus` | json | Approval status | +| ↳ `code` | string | Status code \(NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK\) | +| ↳ `name` | string | Localized status name | +| ↳ `owner` | json | Travel request owner | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Owner first name | +| ↳ `lastName` | string | Owner last name | +| ↳ `approver` | json | Approver assigned to the request | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Approver first name | +| ↳ `lastName` | string | Approver last name | +| ↳ `policy` | json | Resource link to the applicable policy | +| ↳ `id` | string | Policy ID | +| ↳ `href` | string | Policy hyperlink | +| ↳ `type` | json | Request type | +| ↳ `code` | string | Request type code | +| ↳ `label` | string | Request type label | +| ↳ `mainDestination` | json | Main destination of the trip | +| ↳ `city` | string | City | +| ↳ `countryCode` | string | ISO country code | +| ↳ `countrySubDivisionCode` | string | ISO country sub-division code | +| ↳ `name` | string | Destination name | +| ↳ `totalApprovedAmount` | json | Total approved amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalPostedAmount` | json | Total posted amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalRemainingAmount` | json | Total remaining amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `operations` | array | Available workflow actions | +| ↳ `rel` | string | Operation name | +| ↳ `href` | string | Operation URL | +| ↳ `expenses` | array | Expected expenses attached to the request | +| ↳ `highestExceptionLevel` | string | Highest exception level \(NONE, WARNING, ERROR\) | +| ↳ `travelAgency` | json | Travel agency reference | +| ↳ `id` | string | Agency identifier | +| ↳ `href` | string | Agency URL | +| ↳ `template` | string | Template URL | +| ↳ `custom1` | json | Custom field 1 | +| ↳ `custom2` | json | Custom field 2 | +| ↳ `custom3` | json | Custom field 3 | +| ↳ `custom4` | json | Custom field 4 | + +### `sap_concur_create_user` + +Create a new user identity (POST /profile/identity/v4.1/Users). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `body` | json | Yes | SCIM User payload \(schemas, userName, name, emails, active, etc.\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created SCIM User payload | + +### `sap_concur_delete_expected_expense` + +Delete an expected expense (DELETE /travelrequest/v4/expenses/{expenseUuid}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `expenseUuid` | string | Yes | Expected expense UUID to delete | +| `userId` | string | No | User UUID acting on the request \(required when using a Company JWT, optional otherwise\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty body on 204 No Content when the expected expense is deleted. Error payload otherwise. | + +### `sap_concur_delete_expense` + +Delete an expense (DELETE /expensereports/v4/reports/{reportId}/expenses/{expenseId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty body on success \(HTTP 204 No Content\). Error details when status is non-2xx | + +### `sap_concur_delete_expense_report` + +Delete an expense report (DELETE /expensereports/v4/reports/{reportId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `reportId` | string | Yes | Expense report ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty \(204 No Content\) | + +### `sap_concur_delete_list_item` + +Delete a list item (DELETE /list/v4/items/{itemId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `itemId` | string | Yes | List item UUID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty body on success \(HTTP 204 No Content\). Error details when status is non-2xx | + +### `sap_concur_delete_travel_request` + +Delete a travel request (DELETE /travelrequest/v4/requests/{requestUuid}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID to delete | +| `userId` | string | No | Optional Concur user UUID — required when impersonating another user | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Concur delete response payload \(boolean true on 200 OK\) | + +### `sap_concur_delete_user` + +Delete a user identity (DELETE /profile/identity/v4.1/Users/{id}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userUuid` | string | Yes | User UUID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Deletion response — empty body on HTTP 204 No Content | + +### `sap_concur_get_allocation` + +Get a single allocation (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/allocations/{allocationId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `allocationId` | string | Yes | Allocation ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Allocation detail payload | +| ↳ `allocationId` | string | Unique allocation identifier | +| ↳ `accountCode` | string | Ledger account code | +| ↳ `overLimitAccountCode` | string | Account code applied to amounts over the per-allocation limit | +| ↳ `percentage` | number | Allocation percentage | +| ↳ `allocationAmount` | json | Allocation amount \(value, currencyCode\) | +| ↳ `value` | number | Amount value | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `approvedAmount` | json | Pro-rated approved amount \(value, currencyCode\) | +| ↳ `value` | number | Amount value | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `claimedAmount` | json | Requested reimbursement amount \(value, currencyCode\) | +| ↳ `value` | number | Amount value | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `customData` | array | Custom field values \(id, value, isValid\) | +| ↳ `expenseId` | string | Associated expense identifier | +| ↳ `isSystemAllocation` | boolean | True when system-managed | +| ↳ `isPercentEdited` | boolean | True when the percentage was manually edited | + +### `sap_concur_get_budget` + +Get a budget item header by ID (GET /budget/v4/budgetItemHeader/{id}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `budgetId` | string | Yes | Budget item header ID \(syncguid\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Budget header detail payload | +| ↳ `id` | string | Budget item header ID | +| ↳ `name` | string | Admin-facing budget name | +| ↳ `description` | string | User-friendly display name | +| ↳ `budgetItemStatusType` | string | Status: OPEN, CLOSED, or REMOVED | +| ↳ `budgetType` | string | Type: PERSONAL_USE, BUDGET, RESTRICTED, or TEAM | +| ↳ `periodType` | string | Period type: YEARLY, QUARTERLY, MONTHLY, or DATE_RANGE | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `isTest` | boolean | Test budget flag | +| ↳ `active` | boolean | Display availability flag | +| ↳ `owned` | boolean | Caller ownership flag | +| ↳ `annualBudget` | number | Total annual budget amount | +| ↳ `createdDate` | string | UTC creation timestamp | +| ↳ `lastModifiedDate` | string | UTC modification timestamp | +| ↳ `fiscalYear` | json | Fiscal year reference \(id, name, startDate, endDate, status\) | +| ↳ `budgetAmounts` | json | Aggregate spend amounts \(pendingAmount, spendAmount, unExpensedAmount, availableAmount, adjustedBudgetAmount, consumedPercent, threshold\) | +| ↳ `owner` | json | Owner user \(externalUserCUUID, employeeUuid, email, employeeId, name\) | +| ↳ `budgetManagers` | array | Manager user objects | +| ↳ `budgetApprovers` | array | Approver user objects | +| ↳ `budgetViewers` | array | Viewer user objects | +| ↳ `budgetTeamMembers` | array | Team member entries \(budgetPerson, startDate, endDate, active, status\) | +| ↳ `budgetCategory` | json | Linked category \(id, name, description, statusType\) | +| ↳ `costObjects` | array | Tracking field values \(fieldDefinitionId, code, value, operator\) | +| ↳ `budgetItemDetails` | array | Per-period detail entries \(id, currencyCode, amount, budgetItemDetailStatusType, fiscalPeriod, budgetAmounts\) | +| ↳ `dateRange` | json | Date range for DATE_RANGE budgets \(startDate, endDate\) | + +### `sap_concur_get_cash_advance` + +Get a cash advance (GET /cashadvance/v4.1/cashadvances/{cashadvanceId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `cashAdvanceId` | string | Yes | Cash advance ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Cash advance detail payload | +| ↳ `cashAdvanceId` | string | Unique identifier of the cash advance | +| ↳ `name` | string | Cash advance name | +| ↳ `purpose` | string | Purpose for the cash advance | +| ↳ `comment` | string | Comment recorded on the cash advance | +| ↳ `accountCode` | string | Account code linked to the employee | +| ↳ `requestDate` | string | Datetime the cash advance was requested \(UTC, YYYY-MM-DD hh:mm:ss\) | +| ↳ `issuedDate` | string | Datetime the cash advance was issued \(UTC, YYYY-MM-DD hh:mm:ss\) | +| ↳ `lastModifiedDate` | string | Datetime the cash advance was last modified \(UTC, YYYY-MM-DD hh:mm:ss\) | +| ↳ `hasReceipts` | boolean | Whether the cash advance has receipts | +| ↳ `reimbursementCurrency` | string | Reimbursement currency \(3-letter ISO 4217 currency code\) | +| ↳ `amountRequested` | json | Amount requested for the cash advance | +| ↳ `amount` | string | Requested amount value | +| ↳ `currency` | string | 3-letter ISO 4217 currency code | +| ↳ `availableBalance` | json | Unsubmitted balance for the cash advance | +| ↳ `amount` | string | Balance amount | +| ↳ `currency` | string | 3-letter ISO 4217 currency code | +| ↳ `exchangeRate` | json | Exchange rate that applies to the cash advance | +| ↳ `value` | string | Exchange rate value | +| ↳ `operation` | string | Exchange rate operation \(MULTIPLY\) | +| ↳ `approvalStatus` | json | Approval status of the cash advance | +| ↳ `code` | string | Status code | +| ↳ `name` | string | Status display name | +| ↳ `paymentType` | json | Payment type for the cash advance | +| ↳ `paymentCode` | string | Payment type code | +| ↳ `description` | string | Payment method description | + +### `sap_concur_get_exchange_rate` + +Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body: { currency_sets: [{ from_crn_code, to_crn_code, start_date: + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `body` | json | Yes | Bulk upload body: \{ currency_sets: \[\{ from_crn_code, to_crn_code, start_date: "YYYY-MM-DD", rate \}\] \} \(max 100 entries\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Bulk-upload exchange rate response \(Exchange Rate v4\) | +| ↳ `overallStatus` | string | Overall result status for the bulk upload \(e.g. SUCCESS, FAILURE\) | +| ↳ `message` | string | Top-level result message | +| ↳ `currencySets` | json | Per-row results: array of \{ from_crn_code, to_crn_code, start_date, rate, statusCode, statusMessage \} | + +### `sap_concur_get_expected_expense` + +Get an expected expense (GET /travelrequest/v4/expenses/{expenseUuid}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `expenseUuid` | string | Yes | Expected expense UUID | +| `userId` | string | No | User UUID acting on the request \(optional\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Expected expense payload | +| ↳ `id` | string | Expected expense identifier | +| ↳ `href` | string | Self-link | +| ↳ `expenseType` | json | Expense type \{id, name\} | +| ↳ `transactionDate` | string | Transaction date | +| ↳ `transactionAmount` | json | Transaction amount \{value, currencyCode\} | +| ↳ `postedAmount` | json | Posted amount \{value, currencyCode\} | +| ↳ `approvedAmount` | json | Approved amount \{value, currencyCode\} | +| ↳ `remainingAmount` | json | Remaining amount on the expected expense | +| ↳ `businessPurpose` | string | Business purpose of the expense | +| ↳ `location` | json | Location \{id, name, city, countryCode, countrySubDivisionCode, iataCode, locationType\} | +| ↳ `exchangeRate` | json | Exchange rate \{value, operation\} | +| ↳ `allocations` | json | Budget allocations array | +| ↳ `tripData` | json | Trip data \{agencyBooked, selfBooked, tripType \(ONE_WAY\|ROUND_TRIP\), legs\[\{id, returnLeg, startDate, startTime, startLocationDetail, startLocation, endLocation, class \{code,value\}, travelExceptionReasonCodes\}\], segmentType \{category, code\}\} | +| ↳ `parentRequest` | json | Parent travel request resource link \{href, id\} | +| ↳ `comments` | json | Comments sub-resource link \{href, id\} | + +### `sap_concur_get_expense` + +Get a single expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER, MANAGER, or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Expense detail \(ReportExpenseDetail\) payload | +| ↳ `expenseId` | string | Expense identifier | +| ↳ `allocationSetId` | string | Identifier of the associated allocation set | +| ↳ `allocationState` | string | FULLY_ALLOCATED, NOT_ALLOCATED, or PARTIALLY_ALLOCATED | +| ↳ `expenseType` | json | Expense type \{id, name, code, isDeleted\} | +| ↳ `paymentType` | json | Payment type \{id, name, code\} | +| ↳ `expenseSource` | string | Source of the expense \(CASH, CCARD, EBOOKING, etc.\) | +| ↳ `transactionDate` | string | Transaction date \(YYYY-MM-DD\) | +| ↳ `budgetAccrualDate` | string | Budget accrual date | +| ↳ `transactionAmount` | json | Transaction amount \{currencyCode, value\} | +| ↳ `postedAmount` | json | Posted amount in report currency \{currencyCode, value\} | +| ↳ `claimedAmount` | json | Non-personal claimed amount \{currencyCode, value\} | +| ↳ `approvedAmount` | json | Approved amount \{currencyCode, value\} | +| ↳ `approverAdjustedAmount` | json | Total amount adjusted by the approver | +| ↳ `exchangeRate` | json | Exchange rate \{value, operation\} | +| ↳ `vendor` | json | Vendor info \{id, name, description\} | +| ↳ `location` | json | Location \{id, name, city, countryCode, countrySubDivisionCode\} | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `comment` | string | Free-form comment associated with the expense | +| ↳ `isExpenseBillable` | boolean | Billable flag | +| ↳ `isPersonalExpense` | boolean | Personal-expense flag | +| ↳ `isExpenseRejected` | boolean | Whether the expense was rejected | +| ↳ `isExcludedFromCashAdvanceByUser` | boolean | Whether the user excluded this from cash advance | +| ↳ `isImageRequired` | boolean | Whether a receipt image is required | +| ↳ `isPaperReceiptRequired` | boolean | Whether a paper receipt is required | +| ↳ `isPaperReceiptReceived` | boolean | Whether a paper receipt was received | +| ↳ `isAutoCreated` | boolean | Auto-creation indicator | +| ↳ `hasBlockingExceptions` | boolean | Whether submission-blocking exceptions exist | +| ↳ `hasExceptions` | boolean | Whether any exceptions exist | +| ↳ `hasMissingReceiptDeclaration` | boolean | Affidavit declaration status | +| ↳ `attendeeCount` | number | Number of attendees | +| ↳ `receiptImageId` | string | Identifier of the attached receipt image | +| ↳ `ereceiptImageId` | string | eReceipt image identifier | +| ↳ `receiptType` | json | Receipt \{id, status\} | +| ↳ `imageCertificationStatus` | string | Receipt image processing/certification status | +| ↳ `ticketNumber` | string | Associated travel ticket number | +| ↳ `travel` | json | Travel data \(airline, car rental, hotel, etc.\) | +| ↳ `travelAllowance` | json | Travel allowance association data | +| ↳ `mileage` | json | Mileage details \(odometerStart, odometerEnd, totalDistance, ...\) | +| ↳ `expenseTaxSummary` | json | Aggregated tax data for the expense | +| ↳ `taxRateLocation` | string | Tax rate location: FOREIGN, HOME, or OUT_OF_PROVINCE | +| ↳ `fuelTypeListItem` | json | Fuel type list item \{id, value, isValid\} | +| ↳ `merchantTaxId` | string | Merchant tax identifier | +| ↳ `customData` | json | Array of custom field values \[\{id, value, isValid\}\] | +| ↳ `parentExpenseId` | string | Identifier of the parent expense \(for itemizations\) | +| ↳ `authorizationRequestExpenseId` | string | Linked travel-request expected expense identifier | +| ↳ `jptRouteId` | string | Japan Public Transport route id | +| ↳ `invoiceId` | string | Invoice identifier | +| ↳ `governmentInvoiceId` | string | Government invoice identifier | +| ↳ `lastModifiedDate` | string | Last modified timestamp | +| ↳ `expenseSourceIdentifiers` | json | Source reference identifiers | +| ↳ `links` | json | HATEOAS links for the expense | + +### `sap_concur_get_expense_report` + +Retrieve a single expense report header by id via Expense Report v4 (/expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who owns the report | +| `contextType` | string | Yes | Access context: TRAVELER \(own report\), MANAGER \(report under approval\), PROCESSOR, or PROXY | +| `reportId` | string | Yes | Expense report ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Concur expense report header \(ReportDetails\) | +| ↳ `reportId` | string | Unique report identifier | +| ↳ `reportNumber` | string | Report number | +| ↳ `reportFormId` | string | Report form ID | +| ↳ `policyId` | string | Policy ID applied to the report | +| ↳ `policy` | string | Policy name | +| ↳ `name` | string | Report name | +| ↳ `currencyCode` | string | ISO currency code | +| ↳ `currency` | string | Currency name | +| ↳ `approvalStatus` | string | Approval status name | +| ↳ `approvalStatusId` | string | Approval status identifier | +| ↳ `paymentStatus` | string | Payment status name | +| ↳ `paymentStatusId` | string | Payment status identifier | +| ↳ `ledger` | string | Ledger name | +| ↳ `ledgerId` | string | Ledger identifier | +| ↳ `userId` | string | Owner user UUID | +| ↳ `reportDate` | string | Report date \(YYYY-MM-DD\) | +| ↳ `creationDate` | string | Creation timestamp \(ISO 8601\) | +| ↳ `submitDate` | string | Submit timestamp \(ISO 8601\) or null | +| ↳ `startDate` | string | Report period start \(YYYY-MM-DD\) | +| ↳ `endDate` | string | Report period end \(YYYY-MM-DD\) | +| ↳ `approvedAmount` | json | Amount approved \{ value, currencyCode \} | +| ↳ `claimedAmount` | json | Amount claimed \{ value, currencyCode \} | +| ↳ `reportTotal` | json | Report total \{ value, currencyCode \} | +| ↳ `amountDueEmployee` | json | Amount due employee | +| ↳ `amountDueCompany` | json | Amount due company | +| ↳ `amountDueCompanyCard` | json | Amount due company card | +| ↳ `amountCompanyPaid` | json | Amount company has paid | +| ↳ `personalAmount` | json | Personal portion of the report | +| ↳ `paymentConfirmedAmount` | json | Confirmed payment amount | +| ↳ `amountNotApproved` | json | Amount not approved | +| ↳ `totalAmountPaidEmployee` | json | Total amount paid to employee | +| ↳ `concurAuditStatus` | string | Concur audit status | +| ↳ `isFinancialIntegrationEnabled` | boolean | Whether financial integration is enabled | +| ↳ `isSubmitted` | boolean | Whether the report has been submitted | +| ↳ `isSentBack` | boolean | Whether the report has been sent back | +| ↳ `isReopened` | boolean | Whether the report was reopened | +| ↳ `isReportEverSentBack` | boolean | Whether the report was ever sent back | +| ↳ `canRecall` | boolean | Whether the report can be recalled | +| ↳ `canAddExpense` | boolean | Whether expenses can be added to the report | +| ↳ `canReopen` | boolean | Whether the report can be reopened | +| ↳ `isReceiptImageRequired` | boolean | Whether receipt images are required | +| ↳ `isReceiptImageAvailable` | boolean | Whether receipt images are available | +| ↳ `isPaperReceiptsReceived` | boolean | Whether paper receipts were received | +| ↳ `isPendingDelegatorReview` | boolean | Whether pending delegator review | +| ↳ `isFundsAndGrantsIntegrationEligible` | boolean | Funds and grants eligibility | +| ↳ `hasReceivedCashAdvanceReturns` | boolean | Whether cash advance returns received | +| ↳ `analyticsGroupId` | string | Analytics group ID | +| ↳ `hierarchyNodeId` | string | Hierarchy node ID | +| ↳ `allocationFormId` | string | Allocation form ID | +| ↳ `countryCode` | string | ISO country code | +| ↳ `countrySubDivisionCode` | string | ISO country subdivision code | +| ↳ `country` | string | Country name | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `comment` | string | Header-level comment on the report | +| ↳ `reportVersion` | number | Report version number | +| ↳ `reportType` | string | Report type identifier | +| ↳ `cardProgramStatementPeriodId` | string | Card program statement period ID | +| ↳ `defaultFieldAccess` | string | Default field access \(HD/RO/RW\) | +| ↳ `imageStatus` | string | Image status | +| ↳ `receiptContainerId` | string | Receipt container ID | +| ↳ `receiptStatus` | string | Receipt status | +| ↳ `sponsorId` | string | Sponsor ID | +| ↳ `submitterId` | string | Submitter user ID | +| ↳ `taxConfigId` | string | Tax configuration ID | +| ↳ `redirectFund` | json | Redirect fund object \{ amount, creditCardId \} | +| ↳ `customData` | array | Array of custom data \{ id, value, isValid, listItemUrl \} | +| ↳ `employee` | json | Employee object \{ employeeId, employeeUuid \} | +| ↳ `links` | array | HATEOAS links | + +### `sap_concur_get_itemizations` + +Get expense itemizations (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/itemizations). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context \(TRAVELER per the v4 spec\) | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of itemizations \(ReportExpenseSummary\[\]\) | +| ↳ `id` | string | Itemization identifier | +| ↳ `expenseId` | string | Itemization expense id | +| ↳ `allocations` | array | Allocations applied to the itemization | +| ↳ `expenseType` | json | Expense type \{id, name, code, isDeleted\} | +| ↳ `transactionDate` | string | Transaction date \(YYYY-MM-DD\) | +| ↳ `transactionAmount` | json | Transaction amount | +| ↳ `postedAmount` | json | Posted amount | +| ↳ `approvedAmount` | json | Approved amount | +| ↳ `claimedAmount` | json | Claimed amount | +| ↳ `approverAdjustedAmount` | json | Approver-adjusted amount | +| ↳ `paymentType` | json | Payment type | +| ↳ `vendor` | json | Vendor info | +| ↳ `location` | json | Location info | +| ↳ `allocationState` | string | Allocation state | +| ↳ `allocationSetId` | string | Allocation set identifier | +| ↳ `attendeeCount` | number | Attendee count | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `hasBlockingExceptions` | boolean | Has blocking exceptions | +| ↳ `hasExceptions` | boolean | Has exceptions | +| ↳ `isPersonalExpense` | boolean | Personal expense | +| ↳ `links` | array | HATEOAS links | + +### `sap_concur_get_itinerary` + +Get a single trip/itinerary (GET /api/travel/trip/v1.1/{tripID}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `tripId` | string | Yes | Trip ID | +| `useridType` | string | No | User identifier type \(login, xmlsyncid, uuid\) | +| `useridValue` | string | No | User identifier value \(paired with useridType\) | +| `systemFormat` | string | No | Optional system format \(e.g., GDS\) for the response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Trip detail payload \(Itinerary v1.1\) | +| ↳ `ItinLocator` | string | Concur trip locator \(trip ID\) | +| ↳ `ClientLocator` | string | Client \(booking source\) trip locator | +| ↳ `ItinSourceName` | string | Booking source name | +| ↳ `BookedVia` | string | How the trip was booked \(e.g. ConcurTravel, Direct\) | +| ↳ `TripName` | string | Trip name | +| ↳ `Status` | string | Trip status \(e.g. Confirmed, Cancelled\) | +| ↳ `Description` | string | Trip description | +| ↳ `Comments` | string | Comments attached to the trip | +| ↳ `CancelComments` | string | Cancellation comments \(when applicable\) | +| ↳ `ProjectName` | string | Associated project name | +| ↳ `StartDateUtc` | string | Trip start datetime in UTC | +| ↳ `EndDateUtc` | string | Trip end datetime in UTC | +| ↳ `StartDateLocal` | string | Trip start datetime in local time | +| ↳ `EndDateLocal` | string | Trip end datetime in local time | +| ↳ `DateCreatedUtc` | string | Trip creation timestamp \(UTC\) | +| ↳ `DateModifiedUtc` | string | Trip last-modified timestamp \(UTC\) | +| ↳ `DateBookedLocal` | string | Booking date in local time | +| ↳ `UserLoginId` | string | Login id of the trip owner | +| ↳ `BookedByFirstName` | string | First name of the booker | +| ↳ `BookedByLastName` | string | Last name of the booker | +| ↳ `IsPersonal` | boolean | Whether the trip is flagged personal | +| ↳ `RuleViolations` | array | Travel rule violations attached to the trip | +| ↳ `Bookings` | array | Bookings \(air/hotel/car/rail\) attached to the trip | + +### `sap_concur_get_list` + +Get a single custom list (GET /list/v4/lists/{listId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `listId` | string | Yes | List ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | List detail payload | +| ↳ `id` | string | Unique identifier \(UUID\) of the list | +| ↳ `value` | string | Name of the list | +| ↳ `levelCount` | number | Number of levels in the list | +| ↳ `searchCriteria` | string | Search attribute \(TEXT or CODE\) | +| ↳ `displayFormat` | string | Display order \(\(CODE\) TEXT or TEXT \(CODE\)\) | +| ↳ `category` | json | List category | +| ↳ `id` | string | Category UUID | +| ↳ `type` | string | Category type | +| ↳ `isReadOnly` | boolean | Whether the list is read-only | +| ↳ `isDeleted` | boolean | Whether the list has been deleted | +| ↳ `managedBy` | string | Identifier of the managing application or service | +| ↳ `externalThreshold` | number | Threshold from where the level starts being external | + +### `sap_concur_get_list_item` + +Get a single list item (GET /list/v4/items/{itemId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `itemId` | string | Yes | List item ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | List item detail payload | +| ↳ `id` | string | List item UUID | +| ↳ `code` | string | Long code format for the item | +| ↳ `shortCode` | string | Short code identifier | +| ↳ `value` | string | Display value of the item | +| ↳ `parentId` | string | Parent item UUID \(omitted for first-level items\) | +| ↳ `level` | number | Hierarchy level \(1 for root items\) | +| ↳ `isDeleted` | boolean | Deletion status across all containing lists | +| ↳ `lists` | array | Lists containing this item | +| ↳ `id` | string | List UUID | +| ↳ `hasChildren` | boolean | Whether this item has children in the list | + +### `sap_concur_get_purchase_request` + +Get a purchase request by ID (GET /purchaserequest/v4/purchaserequests/{id}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `purchaseRequestId` | string | Yes | Purchase request ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Purchase request detail payload | +| ↳ `purchaseRequestId` | string | Unique identifier of the purchase request | +| ↳ `purchaseRequestNumber` | string | Human-readable purchase request number | +| ↳ `purchaseRequestQueueStatus` | string | Queue status of the purchase request | +| ↳ `purchaseRequestWorkflowStatus` | string | Workflow status of the purchase request | +| ↳ `purchaseOrders` | array | Purchase orders generated from the request | +| ↳ `purchaseOrderNumber` | string | Purchase order number | +| ↳ `purchaseRequestExceptions` | array | Exceptions raised on the purchase request | +| ↳ `eventCode` | string | Event code | +| ↳ `exceptionCode` | string | Exception code | +| ↳ `isCleared` | boolean | Whether the exception has been cleared | +| ↳ `prExceptionId` | string | Identifier of the exception record | +| ↳ `message` | string | Exception message | + +### `sap_concur_get_receipt` + +Get a single receipt by ID (GET /receipts/v4/{receiptId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `receiptId` | string | Yes | Receipt ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Receipt detail payload | +| ↳ `id` | string | Receipt identifier | +| ↳ `userId` | string | Owning user UUID | +| ↳ `dateTimeReceived` | string | Timestamp when the receipt was received \(ISO 8601\) | +| ↳ `receipt` | json | Parsed receipt JSON object | +| ↳ `image` | string | Receipt image URL or data reference | +| ↳ `validationSchema` | string | Schema used to validate the receipt | +| ↳ `self` | string | URL to this receipt resource | +| ↳ `template` | string | URL template for receipts | + +### `sap_concur_get_receipt_status` + +Get receipt processing status (GET /receipts/v4/status/{receiptId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `receiptId` | string | Yes | Receipt ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Receipt status payload | +| ↳ `status` | string | Processing status: ACCEPTED, PROCESSING, PROCESSED, or FAILED | +| ↳ `logs` | array | Array of log entries | +| ↳ `logLevel` | string | Log level | +| ↳ `message` | string | Log message | +| ↳ `timestamp` | string | Log timestamp | + +### `sap_concur_get_travel_profile` + +Get a travel profile (GET /api/travelprofile/v2.0/profile). Returns the calling user by default; pass userid_type and userid_value to impersonate. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `useridType` | string | No | Identifier type: login, xmlsyncid, or uuid | +| `useridValue` | string | No | Identifier value \(login id, xml sync id, or UUID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Travel profile payload. Concur returns XML; downstream may parse it to a best-effort JSON object with the documented top-level sections. | +| ↳ `General` | json | General profile info \(NamePrefix, FirstName, MiddleName, LastName, NameSuffix, JobTitle, CompanyEmployeeID, EmailAddress, RuleClass, TravelConfigID, etc.\) | +| ↳ `Telephones` | json | Telephone numbers \(Telephone\[\] with Type, CountryCode, PhoneNumber, etc.\) | +| ↳ `Addresses` | json | Address records \(Address\[\] with Type, Street, City, StateProvince, etc.\) | +| ↳ `DriversLicenses` | array | Drivers license records | +| ↳ `NationalIDs` | array | National ID records | +| ↳ `EmailAddresses` | json | Email addresses \(EmailAddress\[\] with Type, Address, Contact, Verified\) | +| ↳ `EmergencyContact` | json | Emergency contact \(Name, Relationship, Phones, Address\) | +| ↳ `Air` | json | Air travel preferences \(HomeAirport, Seat, Meal, AirOther, AirMemberships\) | +| ↳ `Rail` | json | Rail preferences \(Seat, Coach, Berth, Other, RailMemberships\) | +| ↳ `Hotel` | json | Hotel preferences \(SmokingCode, RoomType, HotelOther, HotelMemberships, Accessibility flags\) | +| ↳ `Car` | json | Car rental preferences \(CarSmokingCode, CarType, CarMemberships, etc.\) | +| ↳ `CustomFields` | json | Custom-defined fields configured by the company | +| ↳ `RatePreferences` | json | Rate preferences \(e.g. AAA, AARP, government, military rates\) | +| ↳ `DiscountCodes` | json | Discount codes available to the traveler | +| ↳ `HasNoPassport` | boolean | Whether the traveler has no passport on file | +| ↳ `Roles` | json | Role assignments \(TravelManager, Assistant, etc.\) | +| ↳ `Sponsors` | json | Sponsor information for guest travelers | +| ↳ `TSAInfo` | json | TSA SecureFlight info \(Gender, DateOfBirth, NoMiddleName, etc.\) | +| ↳ `Passports` | json | Passport documents \(Passport\[\] with PassportNumber, Country, Expiration\) | +| ↳ `Visas` | json | Visa documents \(Visa\[\] with VisaNationality, VisaNumber, etc.\) | +| ↳ `UnusedTickets` | json | Unused ticket records | +| ↳ `SouthwestUnusedTickets` | json | Southwest-specific unused ticket records | +| ↳ `AdvantageMemberships` | json | Advantage program memberships | +| ↳ `XmlSyncId` | string | XML sync identifier for the user | +| ↳ `LoginId` | string | Concur login id | +| ↳ `ProfileLastModifiedUTC` | string | UTC timestamp the profile was last modified | + +### `sap_concur_get_travel_request` + +Get a single travel request (GET /travelrequest/v4/requests/{requestUuid}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID | +| `userId` | string | No | Optional Concur user UUID — required when impersonating another user | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Travel request detail payload | +| ↳ `id` | string | Travel request UUID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `requestId` | string | Public-facing request ID \(4-6 alphanumeric characters\) | +| ↳ `name` | string | Request name | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `comment` | string | Last attached comment | +| ↳ `creationDate` | string | Creation timestamp | +| ↳ `lastModified` | string | Last modification timestamp | +| ↳ `submitDate` | string | Last submission timestamp | +| ↳ `authorizedDate` | string | Date when approval was completed | +| ↳ `approvalLimitDate` | string | Required approval deadline | +| ↳ `startDate` | string | Trip start date \(ISO 8601\) | +| ↳ `endDate` | string | Trip end date \(ISO 8601\) | +| ↳ `startTime` | string | Trip start time \(HH:mm\) | +| ↳ `endTime` | string | Trip end time \(HH:mm\) | +| ↳ `pnr` | string | Passenger record number | +| ↳ `approved` | boolean | Whether the request is approved | +| ↳ `pendingApproval` | boolean | Pending approval flag | +| ↳ `closed` | boolean | Closed flag | +| ↳ `everSentBack` | boolean | Ever-sent-back flag | +| ↳ `canceledPostApproval` | boolean | Canceled after approval flag | +| ↳ `isParentRequest` | boolean | Parent request flag | +| ↳ `parentRequestId` | string | Parent budget request ID | +| ↳ `allocationFormId` | string | Allocation form identifier | +| ↳ `highestExceptionLevel` | string | Highest exception level \(WARNING, ERROR, NONE\) | +| ↳ `approvalStatus` | json | Approval status | +| ↳ `code` | string | Status code \(NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK\) | +| ↳ `name` | string | Localized status name | +| ↳ `owner` | json | Travel request owner | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Owner first name | +| ↳ `lastName` | string | Owner last name | +| ↳ `approver` | json | Approver assigned to the request | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Approver first name | +| ↳ `lastName` | string | Approver last name | +| ↳ `policy` | json | Resource link to the applicable policy | +| ↳ `id` | string | Policy ID | +| ↳ `href` | string | Policy hyperlink | +| ↳ `type` | json | Request type | +| ↳ `code` | string | Request type code | +| ↳ `label` | string | Request type label | +| ↳ `mainDestination` | json | Main destination of the trip | +| ↳ `city` | string | City | +| ↳ `countryCode` | string | ISO country code | +| ↳ `countrySubDivisionCode` | string | ISO country sub-division code | +| ↳ `name` | string | Destination name | +| ↳ `totalApprovedAmount` | json | Total approved amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalPostedAmount` | json | Total posted amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalRemainingAmount` | json | Total remaining amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `expenses` | array | Resource links to expected expenses | +| ↳ `cashAdvances` | json | Resource link to cash advances | +| ↳ `id` | string | Resource ID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `comments` | json | Resource link to comments | +| ↳ `id` | string | Resource ID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `exceptions` | json | Resource link to exceptions | +| ↳ `id` | string | Resource ID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `travelAgency` | json | Resource link to travel agency | +| ↳ `id` | string | Resource ID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `parentRequest` | json | Resource link to parent request | +| ↳ `id` | string | Resource ID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `eventRequest` | json | Resource link to parent event request | +| ↳ `id` | string | Resource ID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `operations` | array | Available workflow actions | +| ↳ `rel` | string | Operation name | +| ↳ `href` | string | Operation URL | +| ↳ `expensePolicy` | json | Expense policy reference | +| ↳ `id` | string | Policy identifier | +| ↳ `href` | string | Policy URL | +| ↳ `custom1` | json | Custom field 1 | +| ↳ `custom2` | json | Custom field 2 | +| ↳ `custom3` | json | Custom field 3 | +| ↳ `custom4` | json | Custom field 4 | + +### `sap_concur_get_user` + +Get a single user by UUID (GET /profile/identity/v4.1/Users/{id}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userUuid` | string | Yes | User UUID | +| `attributes` | string | No | Comma-separated SCIM attributes to include in the response | +| `excludedAttributes` | string | No | Comma-separated SCIM attributes to exclude from the response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | SCIM User identity payload | + +### `sap_concur_issue_cash_advance` + +Issue a cash advance (POST /cashadvance/v4.1/cashadvances/{cashadvanceId}/issue). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `cashAdvanceId` | string | Yes | Cash advance ID to issue | +| `body` | json | No | Optional request body | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Issue cash advance result payload | +| ↳ `issuedDate` | string | Datetime the cash advance was issued \(UTC, YYYY-MM-DD hh:mm:ss\) | +| ↳ `status` | json | Cash advance status after the issue action | +| ↳ `code` | string | Status code | +| ↳ `name` | string | Status display name | + +### `sap_concur_list_allocations` + +List allocations on an expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/allocations). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Allocations list payload | + +### `sap_concur_list_attendee_associations` + +List attendees associated with an expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/attendees). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Attendees list payload | +| ↳ `noShowAttendeeCount` | number | Number of unnamed/no-show attendees | +| ↳ `expenseAttendeeList` | array | Attendees associated with the expense, including amounts | +| ↳ `attendeeId` | string | Unique identifier of the attendee | +| ↳ `transactionAmount` | json | Expense portion assigned to this attendee | +| ↳ `value` | number | Numeric amount | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `approvedAmount` | json | Approved amount in report currency | +| ↳ `value` | number | Numeric amount | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `isAmountUserEdited` | boolean | Whether the amount was manually edited | +| ↳ `isTraveling` | boolean | Whether the attendee is traveling \(affects tax calculations\) | +| ↳ `associatedAttendeeCount` | number | Total attendee count; greater than 1 indicates unnamed attendees | +| ↳ `versionNumber` | number | Version number preserving previous attendee state | +| ↳ `customData` | array | Custom field values for the association | +| ↳ `id` | string | Custom field identifier | +| ↳ `value` | string | Custom field value \(max 48 characters\) | +| ↳ `isValid` | boolean | Whether the value passes validation | +| ↳ `listItemUrl` | string | HATEOAS link for list items | + +### `sap_concur_list_budget_categories` + +List budget categories (GET /budget/v4/budgetCategory). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Budget categories collection payload | + +### `sap_concur_list_budgets` + +List budget item headers (GET /budget/v4/budgetItemHeader). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `adminView` | boolean | No | When true, returns all budgets the caller can administer \(default false\) | +| `offset` | number | No | Page offset \(Concur returns up to 50 budget headers per page\) | +| `responseSchema` | string | No | Response schema variant: "COMPACT" returns a smaller payload | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Budget headers collection payload | +| ↳ `offset` | number | Page offset | +| ↳ `limit` | number | Page size | +| ↳ `totalCount` | number | Total result count | + +### `sap_concur_list_exceptions` + +List exceptions on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/exceptions). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER, MANAGER, or PROXY | +| `reportId` | string | Yes | Expense report ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of report header exception entries | +| ↳ `exceptionCode` | string | Unique exception code | +| ↳ `exceptionVisibility` | string | Visibility scope: ALL, APPROVER_PROCESSOR, or PROCESSOR | +| ↳ `isBlocking` | boolean | Whether the exception prevents report submission | +| ↳ `message` | string | Human-readable description of the exception | +| ↳ `expenseId` | string | Related expense entry ID | +| ↳ `allocationId` | string | Related allocation ID, if any | +| ↳ `parentExpenseId` | string | Parent expense ID for itemized entries | + +### `sap_concur_list_expected_expenses` + +List expected expenses on a travel request (GET /travelrequest/v4/requests/{requestUuid}/expenses). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID | +| `userId` | string | No | User UUID acting on the request \(optional\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Array of expected expense objects. Each entry includes id, href, expenseType \{id,name\}, transactionDate, transactionAmount, postedAmount, approvedAmount, remainingAmount, businessPurpose, location, exchangeRate, allocations, tripData, parentRequest \{href, id\}, comments \{href, id\}. | + +### `sap_concur_list_expenses` + +List expenses on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context \(TRAVELER per the v4 spec\) | +| `reportId` | string | Yes | Expense report ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of expense summary entries \(ReportExpenseSummary\[\]\) | +| ↳ `expenseId` | string | Expense identifier | +| ↳ `expenseType` | json | Expense type \{id, name, code, isDeleted\} | +| ↳ `transactionDate` | string | Transaction date \(YYYY-MM-DD\) | +| ↳ `transactionAmount` | json | Transaction amount \{currencyCode, value\} | +| ↳ `postedAmount` | json | Posted amount | +| ↳ `approvedAmount` | json | Approved amount | +| ↳ `claimedAmount` | json | Claimed amount | +| ↳ `approverAdjustedAmount` | json | Approver-adjusted amount | +| ↳ `paymentType` | json | Payment type \{id, name, code\} | +| ↳ `vendor` | json | Vendor info | +| ↳ `location` | json | Location info | +| ↳ `allocationState` | string | Allocation state | +| ↳ `allocationSetId` | string | Allocation set identifier | +| ↳ `attendeeCount` | number | Attendee count | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `hasBlockingExceptions` | boolean | Has submission-blocking exceptions | +| ↳ `hasExceptions` | boolean | Has exceptions | +| ↳ `hasMissingReceiptDeclaration` | boolean | Has missing-receipt declaration | +| ↳ `isAutoCreated` | boolean | Auto-created | +| ↳ `isPersonalExpense` | boolean | Personal-expense flag | +| ↳ `isImageRequired` | boolean | Receipt image required | +| ↳ `isPaperReceiptRequired` | boolean | Paper receipt required | +| ↳ `imageCertificationStatus` | string | Receipt image certification status | +| ↳ `receiptImageId` | string | Receipt image identifier | +| ↳ `ereceiptImageId` | string | eReceipt image identifier | +| ↳ `ticketNumber` | string | Ticket number | +| ↳ `exchangeRate` | json | Exchange rate | +| ↳ `travelAllowance` | json | Travel allowance | +| ↳ `expenseSourceIdentifiers` | json | Expense source identifiers | +| ↳ `links` | array | HATEOAS links | + +### `sap_concur_list_expense_reports` + +List expense reports (GET /api/v3.0/expense/reports). Returns a v3 envelope { Items, NextPage }. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(us, us2, eu, eu2, cn, emea — defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `user` | string | No | Filter by a specific user \(login id or user identifier\). | +| `submitDateBefore` | string | No | Filter to reports submitted on or before this date \(YYYY-MM-DD\) | +| `submitDateAfter` | string | No | Filter to reports submitted on or after this date \(YYYY-MM-DD\) | +| `paidDateBefore` | string | No | Filter to reports paid on or before this date \(YYYY-MM-DD\) | +| `paidDateAfter` | string | No | Filter to reports paid on or after this date \(YYYY-MM-DD\) | +| `modifiedDateBefore` | string | No | Filter to reports last modified on or before this date \(YYYY-MM-DD\) | +| `modifiedDateAfter` | string | No | Filter to reports last modified on or after this date \(YYYY-MM-DD\) | +| `createDateBefore` | string | No | Filter to reports created on or before this date \(YYYY-MM-DD\) | +| `createDateAfter` | string | No | Filter to reports created on or after this date \(YYYY-MM-DD\) | +| `approvalStatusCode` | string | No | Filter by approval status code \(e.g. A_NOTF, A_PEND, A_APPR\) | +| `paymentStatusCode` | string | No | Filter by payment status code | +| `currencyCode` | string | No | Filter by ISO currency code \(e.g. USD, EUR\) | +| `approverLoginID` | string | No | Filter by approver login ID | +| `limit` | number | No | Number of records per page \(default 25, max 100\) | +| `offset` | string | No | Opaque cursor token returned by a prior call \(NextPage\). | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Concur v3 expense reports envelope | +| ↳ `Items` | array | Array of report header objects | +| ↳ `ID` | string | Report ID | +| ↳ `Name` | string | Report name | +| ↳ `OwnerLoginID` | string | Owner login ID | +| ↳ `OwnerName` | string | Owner display name | +| ↳ `Total` | number | Report total | +| ↳ `TotalApprovedAmount` | number | Total approved amount | +| ↳ `TotalClaimedAmount` | number | Total claimed amount | +| ↳ `AmountDueEmployee` | number | Amount due employee | +| ↳ `CurrencyCode` | string | ISO currency code | +| ↳ `ApprovalStatusName` | string | Approval status name | +| ↳ `ApprovalStatusCode` | string | Approval status code | +| ↳ `PaymentStatusName` | string | Payment status name | +| ↳ `PaymentStatusCode` | string | Payment status code | +| ↳ `ApproverLoginID` | string | Approver login ID | +| ↳ `ApproverName` | string | Approver display name | +| ↳ `HasException` | boolean | Whether the report has any exception | +| ↳ `ReceiptsReceived` | boolean | Whether paper receipts were received | +| ↳ `CreateDate` | string | Creation date | +| ↳ `SubmitDate` | string | Submit date | +| ↳ `LastModifiedDate` | string | Last modified date | +| ↳ `PaidDate` | string | Paid date | +| ↳ `URI` | string | Self URI | +| ↳ `NextPage` | string | URI of the next page \(use as offset cursor\) | + +### `sap_concur_list_itineraries` + +List travel trips/itineraries (GET /api/travel/trip/v1.1). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `startDate` | string | No | Filter trips starting on/after this date \(YYYY-MM-DD\) | +| `endDate` | string | No | Filter trips ending on/before this date \(YYYY-MM-DD\) | +| `bookingType` | string | No | Filter by booking type \(air, car, hotel, rail, etc.\) | +| `useridType` | string | No | User identifier type \(login, xmlsyncid, uuid\) | +| `useridValue` | string | No | User identifier value \(paired with useridType\) | +| `itemsPerPage` | number | No | Items per page | +| `page` | number | No | 1-based page number | +| `includeMetadata` | boolean | No | Include paging metadata in the response | +| `includeCanceledTrips` | boolean | No | Include canceled trips in the result set | +| `createdAfterDate` | string | No | Only trips created after this date \(YYYY-MM-DD\) | +| `createdBeforeDate` | string | No | Only trips created before this date \(YYYY-MM-DD\) | +| `lastModifiedDate` | string | No | Only trips modified on/after this date \(YYYY-MM-DD\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Trips list payload \(Itinerary v1.1 ConnectResponse\) | +| ↳ `Metadata` | json | Paging metadata \(when includeMetadata=true\) | +| ↳ `Paging` | json | Pagination details | +| ↳ `TotalPages` | number | Total pages | +| ↳ `TotalItems` | number | Total items | +| ↳ `Page` | number | Current page | +| ↳ `ItemsPerPage` | number | Items per page | +| ↳ `PreviousPageURL` | string | Previous page URL | +| ↳ `NextPageURL` | string | Next page URL | +| ↳ `ItineraryInfoList` | array | List of itinerary summary records | +| ↳ `ItinLocator` | string | Trip locator \(trip ID\) | +| ↳ `ClientLocator` | string | Client trip locator | +| ↳ `ItinSourceName` | string | Booking source name | +| ↳ `BookedVia` | string | Booking channel | +| ↳ `TripName` | string | Trip name | +| ↳ `Status` | string | Trip status | +| ↳ `Description` | string | Trip description | +| ↳ `StartDateUtc` | string | Start \(UTC\) | +| ↳ `EndDateUtc` | string | End \(UTC\) | +| ↳ `StartDateLocal` | string | Start \(local\) | +| ↳ `EndDateLocal` | string | End \(local\) | +| ↳ `DateCreatedUtc` | string | Created \(UTC\) | +| ↳ `DateModifiedUtc` | string | Modified \(UTC\) | +| ↳ `DateBookedLocal` | string | Booked \(local\) | +| ↳ `UserLoginId` | string | Trip owner login id | +| ↳ `BookedByFirstName` | string | Booker first name | +| ↳ `BookedByLastName` | string | Booker last name | +| ↳ `IsPersonal` | boolean | Personal trip flag | + +### `sap_concur_list_lists` + +List custom lists (GET /list/v4/lists). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `page` | number | No | Page number \(1-based; page size is fixed at 100\) | +| `sortBy` | string | No | Sort field: name, levelcount, or listcategory | +| `sortDirection` | string | No | Sort direction: asc or desc | +| `value` | string | No | Filter by list name | +| `categoryType` | string | No | Filter by category type \(mapped to category.type query param\) | +| `isDeleted` | boolean | No | Include deleted lists | +| `levelCount` | number | No | Filter by number of levels | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Paginated lists collection | +| ↳ `content` | array | Lists in the current page | +| ↳ `id` | string | List UUID | +| ↳ `value` | string | Name of the list | +| ↳ `levelCount` | number | Number of levels in the list | +| ↳ `searchCriteria` | string | Search attribute \(TEXT or CODE\) | +| ↳ `displayFormat` | string | Display order \(\(CODE\) TEXT or TEXT \(CODE\)\) | +| ↳ `category` | json | List category | +| ↳ `id` | string | Category UUID | +| ↳ `type` | string | Category type | +| ↳ `isReadOnly` | boolean | Whether the list is read-only | +| ↳ `isDeleted` | boolean | Whether the list has been deleted | +| ↳ `managedBy` | string | Managing application or service identifier | +| ↳ `externalThreshold` | number | Threshold from where the level starts being external | +| ↳ `page` | json | Pagination metadata | +| ↳ `number` | number | Current page number | +| ↳ `size` | number | Items per page | +| ↳ `totalElements` | number | Total item count | +| ↳ `totalPages` | number | Total page count | +| ↳ `links` | array | Navigation links \(next, previous, first, last\) | +| ↳ `rel` | string | Link relation | +| ↳ `href` | string | Link URL | + +### `sap_concur_list_list_items` + +List the top-level items (children) for a custom list (GET /list/v4/lists/{listId}/children). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `listId` | string | Yes | List ID | +| `page` | number | No | Page number \(1-based; page size is fixed at 100\) | +| `sortBy` | string | No | Sort field: value or shortCode | +| `sortDirection` | string | No | Sort direction: asc or desc | +| `hasChildren` | boolean | No | Include only items that have children | +| `isDeleted` | boolean | No | Include deleted items | +| `shortCode` | string | No | Filter by short code | +| `value` | string | No | Filter by display value | +| `shortCodeOrValue` | string | No | Filter by short code OR value | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Paginated list items collection | +| ↳ `content` | array | List items in the current page | +| ↳ `id` | string | List item UUID | +| ↳ `code` | string | Long code format for the item | +| ↳ `shortCode` | string | Short code identifier | +| ↳ `value` | string | Display value of the item | +| ↳ `parentId` | string | Parent item UUID \(omitted for first-level items\) | +| ↳ `level` | number | Hierarchy level \(1 for root items\) | +| ↳ `isDeleted` | boolean | Deletion status across all containing lists | +| ↳ `lists` | array | Lists containing this item | +| ↳ `id` | string | List UUID | +| ↳ `hasChildren` | boolean | Whether this item has children in the list | +| ↳ `page` | json | Pagination metadata | +| ↳ `number` | number | Current page number | +| ↳ `size` | number | Items per page | +| ↳ `totalElements` | number | Total item count | +| ↳ `totalPages` | number | Total page count | +| ↳ `links` | array | Navigation links \(next, previous, first, last\) | +| ↳ `rel` | string | Link relation | +| ↳ `href` | string | Link URL | + +### `sap_concur_list_receipts` + +List receipts for a user (GET /receipts/v4/users/{userId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of e-receipt objects | +| ↳ `id` | string | Receipt id | +| ↳ `userId` | string | Owner user UUID | +| ↳ `dateTimeReceived` | string | Timestamp the receipt was received | +| ↳ `receipt` | json | Structured receipt data | +| ↳ `image` | string | Receipt image URL or reference | +| ↳ `validationSchema` | string | Validation schema URI | +| ↳ `self` | string | Self URL | +| ↳ `template` | string | Template URL | + +### `sap_concur_list_report_comments` + +List comments on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/comments). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER, MANAGER, or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `includeAllComments` | boolean | No | Include comments from all expenses in the report \(default false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of report comment entries | +| ↳ `comment` | string | Comment text | +| ↳ `creationDate` | string | Comment creation timestamp \(ISO 8601\) | +| ↳ `expenseId` | string | Related expense entry ID | +| ↳ `isAuditorComment` | boolean | Whether the comment was added by an auditor | +| ↳ `isLatest` | boolean | Whether this is the latest comment | +| ↳ `createdForEmployeeId` | string | Employee ID the comment was created for | +| ↳ `author` | json | Comment author | +| ↳ `employeeId` | string | Employee identifier | +| ↳ `employeeUuid` | string | Employee UUID | +| ↳ `createdForEmployee` | json | Employee the comment was created for | +| ↳ `employeeId` | string | Employee identifier | +| ↳ `employeeUuid` | string | Employee UUID | +| ↳ `stepInstanceId` | string | Workflow step instance identifier | + +### `sap_concur_list_reports_to_approve` + +List expense reports awaiting approval (GET /expensereports/v4/users/{userId}/context/MANAGER/reportsToApprove). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Manager user UUID | +| `contextType` | string | No | Access context: must be MANAGER \(default\) | +| `sort` | string | No | Report field name to sort by \(e.g., reportDate\) | +| `order` | string | No | Sort direction: asc or desc | +| `includeDelegateApprovals` | boolean | No | Whether to include reports the caller can approve as a delegate | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of reports awaiting approval \(ReportToApprove\[\]\) | +| ↳ `reportId` | string | Unique report identifier | +| ↳ `name` | string | Report name | +| ↳ `reportDate` | string | Report date \(YYYY-MM-DD\) | +| ↳ `reportNumber` | string | User-friendly report number | +| ↳ `submitDate` | string | Submission timestamp \(ISO 8601 UTC\) | +| ↳ `approver` | json | Approver employee \{ employeeId, employeeUuid \} | +| ↳ `employee` | json | Report owner employee \{ employeeId, employeeUuid \} | +| ↳ `amountDueEmployee` | json | Amount due employee \{ value, currencyCode \} | +| ↳ `claimedAmount` | json | Total claimed amount \{ value, currencyCode \} | +| ↳ `totalApprovedAmount` | json | Total approved amount \{ value, currencyCode \} | +| ↳ `hasExceptions` | boolean | Whether the report has exceptions | +| ↳ `reportType` | string | Report creation method identifier | +| ↳ `links` | array | HATEOAS links | + +### `sap_concur_get_request_cash_advance` + +Get a single cash advance assigned to a travel request (GET /travelrequest/v4/cashadvances/{cashAdvanceUuid}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `cashAdvanceUuid` | string | Yes | Cash advance UUID \(returned as part of a travel request\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Cash advance detail | +| ↳ `cashAdvanceId` | string | Unique cash advance identifier | +| ↳ `amountRequested` | json | Requested amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `amount` | number | Amount \(alias\) | +| ↳ `approvalStatus` | json | Approval status | +| ↳ `code` | string | Status code | +| ↳ `name` | string | Status name | +| ↳ `requestDate` | string | Request datetime \(ISO 8601\) | +| ↳ `exchangeRate` | json | Exchange rate | +| ↳ `value` | number | Rate value | +| ↳ `operation` | string | Multiply or divide | + +### `sap_concur_list_travel_profiles_summary` + +List travel profile summaries (GET /api/travelprofile/v2.0/summary). LastModifiedDate is required by Concur. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `lastModifiedDate` | string | Yes | Required ISO 8601 date \(YYYY-MM-DD or full timestamp\) | +| `page` | number | No | 1-based page number | +| `itemsPerPage` | number | No | Items per page \(max 200\) | +| `active` | string | No | Status filter \(sent as Status query param\): "Active" or "Inactive". Omit for all. | +| `travelConfigs` | string | No | Comma-separated travel configuration ids | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Travel profile summary list payload \(Concur returns XML mapped to JSON\) | +| ↳ `Metadata` | json | Paging metadata | +| ↳ `Paging` | json | Pagination details | +| ↳ `TotalPages` | number | Total number of pages | +| ↳ `TotalItems` | number | Total number of items | +| ↳ `Page` | number | Current page | +| ↳ `ItemsPerPage` | number | Items per page | +| ↳ `PreviousPageURL` | string | URL to the previous page | +| ↳ `NextPageURL` | string | URL to the next page | +| ↳ `Data` | array | Array of travel profile summaries | +| ↳ `Status` | string | Status \(Active/Inactive\) | +| ↳ `LoginID` | string | Login identifier | +| ↳ `XmlProfileSyncID` | string | XML profile sync identifier | +| ↳ `ProfileLastModifiedUTC` | string | Last modified timestamp \(UTC\) | +| ↳ `RuleClass` | string | Travel rule class assigned to the profile | +| ↳ `TravelConfigID` | string | Travel configuration identifier | +| ↳ `UUID` | string | Profile UUID | +| ↳ `EmployeeID` | string | Employee ID | +| ↳ `CompanyID` | string | Company ID | + +### `sap_concur_list_travel_request_comments` + +List comments on a travel request (GET /travelrequest/v4/requests/{requestUuid}/comments). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of comment entries | +| ↳ `author` | json | Comment author | +| ↳ `firstName` | string | Author first name | +| ↳ `lastName` | string | Author last name | +| ↳ `creationDateTime` | string | Comment creation timestamp \(ISO 8601\) | +| ↳ `isLatest` | boolean | Whether this is the latest comment | +| ↳ `value` | string | Comment text | + +### `sap_concur_list_travel_requests` + +List travel requests (GET /travelrequest/v4/requests). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `view` | string | No | View filter \(e.g., ALL, ACTIVE, PENDING, TOAPPROVE\) | +| `limit` | number | No | Max number of results per page | +| `start` | number | No | Page start cursor \(offset\) | +| `userId` | string | No | Filter by Concur user UUID | +| `approvedBefore` | string | No | ISO 8601 date — return requests approved before this date | +| `approvedAfter` | string | No | ISO 8601 date — return requests approved after this date | +| `modifiedBefore` | string | No | ISO 8601 date — return requests modified before this date | +| `modifiedAfter` | string | No | ISO 8601 date — return requests modified after this date | +| `sortField` | string | No | Field to sort by: startDate, approvalStatus, or requestId \(default startDate\) | +| `sortOrder` | string | No | Sort order: ASC or DESC \(default DESC\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Travel requests list payload | +| ↳ `data` | array | Array of travel request summaries | +| ↳ `id` | string | Travel request UUID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `requestId` | string | Public-facing request ID | +| ↳ `name` | string | Request name | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `comment` | string | Last attached comment | +| ↳ `creationDate` | string | Creation timestamp | +| ↳ `submitDate` | string | Last submission timestamp | +| ↳ `startDate` | string | Trip start date \(ISO 8601\) | +| ↳ `endDate` | string | Trip end date \(ISO 8601\) | +| ↳ `startTime` | string | Trip start time \(HH:mm\) | +| ↳ `approved` | boolean | Whether the request is approved | +| ↳ `pendingApproval` | boolean | Pending approval flag | +| ↳ `closed` | boolean | Closed flag | +| ↳ `everSentBack` | boolean | Ever-sent-back flag | +| ↳ `canceledPostApproval` | boolean | Canceled after approval flag | +| ↳ `approvalStatus` | json | Approval status | +| ↳ `code` | string | Status code \(NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK\) | +| ↳ `name` | string | Localized status name | +| ↳ `owner` | json | Travel request owner | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Owner first name | +| ↳ `lastName` | string | Owner last name | +| ↳ `approver` | json | Approver assigned to the request | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Approver first name | +| ↳ `lastName` | string | Approver last name | +| ↳ `type` | json | Request type | +| ↳ `code` | string | Request type code | +| ↳ `label` | string | Request type label | +| ↳ `totalApprovedAmount` | json | Total approved amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalPostedAmount` | json | Total posted amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalRemainingAmount` | json | Total remaining amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `expenses` | array | Resource links to expected expenses | +| ↳ `operations` | array | Pagination links \(next, prev, first, last\) | +| ↳ `rel` | string | Link relation | +| ↳ `href` | string | Link target | +| ↳ `method` | string | HTTP method | +| ↳ `name` | string | Link name | + +### `sap_concur_list_users` + +List Concur user identities (GET /profile/identity/v4.1/Users). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `count` | number | No | Max number of users to return \(default 100, max 1000\) | +| `cursor` | string | No | SCIM v4.1 pagination cursor returned by a prior call | +| `attributes` | string | No | Comma-separated list of attributes to include in the response | +| `excludedAttributes` | string | No | Comma-separated list of attributes to exclude from the response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | SCIM ListResponse with Resources array | + +### `sap_concur_move_travel_request` + +Move a travel request through workflow (POST /travelrequest/v4/requests/{requestUuid}/{action}). Valid actions: submit, recall, cancel, approve, sendback, close, reopen. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID | +| `action` | string | Yes | Workflow action: submit, recall, cancel, approve, sendback, close, reopen | +| `userId` | string | No | Optional Concur user UUID — required when impersonating another user | +| `body` | json | No | Optional payload \(e.g., \{ "comment": "..." \}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Workflow transition response payload | +| ↳ `id` | string | Travel request UUID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `approvalStatus` | json | Approval status after the workflow transition | +| ↳ `code` | string | Status code \(NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK\) | +| ↳ `name` | string | Localized status name | +| ↳ `approver` | json | Approver assigned after the transition | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Approver first name | +| ↳ `lastName` | string | Approver last name | +| ↳ `operations` | array | Available follow-up workflow actions | +| ↳ `rel` | string | Link relation | +| ↳ `href` | string | Link target | +| ↳ `method` | string | HTTP method | +| ↳ `name` | string | Link name | + +### `sap_concur_recall_expense_report` + +Recall a submitted expense report (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/recall — supported contexts: TRAVELER, PROXY). No request body is required. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who owns the report | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID to recall | +| `body` | json | No | Optional body. Concur docs don't define a payload for this action; pass an empty object if uncertain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty \(204 No Content\) | + +### `sap_concur_remove_all_attendees` + +Remove all attendees from an expense (DELETE /expensereports/v4/users/{userId}/context/TRAVELER/reports/{reportId}/expenses/{expenseId}/attendees). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty response body \(Concur returns 204 No Content\) | + +### `sap_concur_search_locations` + +Search Concur location reference data (GET /localities/v5/locations). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `searchText` | string | No | Free-text query \(city, airport, landmark, etc.\) | +| `locCode` | string | No | IATA / location code | +| `locationNameId` | string | No | Concur internal location name ID \(UUID\) | +| `locationNameKey` | number | No | Concur internal numeric location name key | +| `countryCode` | string | No | 2-letter ISO 3166-1 country code | +| `subdivisionCode` | string | No | ISO 3166-2:2007 country subdivision \(e.g. US-WA\) | +| `adminRegionId` | string | No | Administrative region ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Localities v5 search response | +| ↳ `locations` | array | Array of matching Location records | +| ↳ `id` | string | Location ID \(UUID\) | +| ↳ `code` | string | IATA / location code | +| ↳ `legacyKey` | number | Legacy numeric location key | +| ↳ `timeZoneOffset` | string | IANA timezone or UTC offset | +| ↳ `active` | boolean | Whether the location is active | +| ↳ `point` | json | Geographic coordinates | +| ↳ `latitude` | number | Latitude | +| ↳ `longitude` | number | Longitude | +| ↳ `names` | array | Localized location names | +| ↳ `id` | string | Name ID | +| ↳ `key` | number | Numeric name key | +| ↳ `locale` | string | Locale tag | +| ↳ `name` | string | Display name | +| ↳ `administrativeRegion` | json | Administrative region \(e.g., metro area\) | +| ↳ `id` | string | Region ID | +| ↳ `name` | string | Region name | +| ↳ `country` | json | Country reference | +| ↳ `id` | string | Country ID | +| ↳ `code` | string | ISO country code | +| ↳ `name` | string | Country name | +| ↳ `subDivision` | json | Country subdivision \(state/province\) | +| ↳ `id` | string | Subdivision ID | +| ↳ `code` | string | ISO subdivision code | +| ↳ `name` | string | Subdivision name | +| ↳ `links` | array | HATEOAS links | + +### `sap_concur_search_users` + +Search users via SCIM .search endpoint (POST /profile/identity/v4.1/Users/.search). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `body` | json | Yes | SCIM search request payload \(\{ schemas, attributes, filter, count, startIndex \}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | SCIM search ListResponse | + +### `sap_concur_send_back_expense_report` + +Send back an expense report to the employee (PATCH /expensereports/v4/users/{userId}/context/MANAGER/reports/{reportId}/sendBack). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Manager user UUID sending back the report | +| `contextType` | string | Yes | Access context: must be MANAGER | +| `reportId` | string | Yes | Expense report ID to send back | +| `body` | json | Yes | Request body — `comment` is required by Concur \(e.g., \{ "comment": "Missing receipt" \}\). Optional fields: `expectedStepCode`, `expectedStepSequence`. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty \(204 No Content\) | + +### `sap_concur_submit_expense_report` + +Submit an expense report into the workflow via Expense Report v4 (PATCH /expensereports/v4/users/{userId}/reports/{reportId}/submit). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who owns the report | +| `reportId` | string | Yes | Expense report ID to submit | +| `body` | json | No | Optional body. Concur docs don't define a payload for this action; pass an empty object if uncertain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty \(204 No Content\) | + +### `sap_concur_update_allocation` + +Update an allocation (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/allocations/{allocationId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY \(write requires expense.report.readwrite\) | +| `reportId` | string | Yes | Expense report ID | +| `allocationId` | string | Yes | Allocation ID to update | +| `body` | json | Yes | Fields to update on the allocation | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty body on success \(Concur returns 204 No Content\) | + +### `sap_concur_update_expected_expense` + +Update an expected expense (PUT /travelrequest/v4/expenses/{expenseUuid}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `expenseUuid` | string | Yes | Expected expense UUID to update | +| `userId` | string | No | User UUID acting on the request \(required when using a Company JWT, optional otherwise\) | +| `body` | json | Yes | Fields to update on the expected expense | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Updated expected expense payload | +| ↳ `id` | string | Expected expense identifier | +| ↳ `href` | string | Self-link | +| ↳ `expenseType` | json | Expense type \{id, name\} | +| ↳ `transactionDate` | string | Transaction date | +| ↳ `transactionAmount` | json | Transaction amount \{value, currencyCode\} | +| ↳ `postedAmount` | json | Posted amount \{value, currencyCode\} | +| ↳ `approvedAmount` | json | Approved amount \{value, currencyCode\} | +| ↳ `remainingAmount` | json | Remaining amount on the expected expense | +| ↳ `businessPurpose` | string | Business purpose of the expense | +| ↳ `location` | json | Location \{id, name, city, countryCode, countrySubDivisionCode, iataCode, locationType\} | +| ↳ `exchangeRate` | json | Exchange rate \{value, operation\} | +| ↳ `allocations` | json | Budget allocations array | +| ↳ `tripData` | json | Trip data \{agencyBooked, selfBooked, tripType \(ONE_WAY\|ROUND_TRIP\), legs\[\{id, returnLeg, startDate, startTime, startLocationDetail, startLocation, endLocation, class \{code,value\}, travelExceptionReasonCodes\}\], segmentType \{category, code\}\} | +| ↳ `parentRequest` | json | Parent travel request resource link \{href, id\} | +| ↳ `comments` | json | Comments sub-resource link \{href, id\} | + +### `sap_concur_update_expense` + +Update an expense (PATCH /expensereports/v4/reports/{reportId}/expenses/{expenseId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID to update | +| `body` | json | Yes | PATCH body. Allowed fields: businessPurpose \(string, max 64\), customData \(CustomData\[\]\), expenseSource \(required: EA\|MOB\|OTHER\|SE\|TA\|TR\|UI\), isExpenseRejected \(boolean\), isPaperReceiptReceived \(boolean\). | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty body on success \(HTTP 204 No Content\). Error details when status is non-2xx | + +### `sap_concur_update_expense_report` + +Update an unsubmitted expense report (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId} — supported contexts: TRAVELER, PROXY). Body fields: businessPurpose, comment, customData, name, etc. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who owns the report | +| `contextType` | string | Yes | Access context: TRAVELER \(own report\) or PROXY \(editing on behalf of another user\) | +| `reportId` | string | Yes | Expense report ID to update | +| `body` | json | Yes | Fields to update on the report | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty \(204 No Content\) | + +### `sap_concur_update_list_item` + +Update a list item (PUT /list/v4/items/{itemId}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `itemId` | string | Yes | List item UUID | +| `body` | json | Yes | List item payload. Required: shortCode, value. Other fields in the body are ignored. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Updated list item | +| ↳ `id` | string | List item UUID | +| ↳ `code` | string | Long code format for the item | +| ↳ `shortCode` | string | Short code identifier | +| ↳ `value` | string | Display value of the item | +| ↳ `parentId` | string | Parent item UUID \(omitted for first-level items\) | +| ↳ `level` | number | Hierarchy level \(1 for root items\) | +| ↳ `isDeleted` | boolean | Deletion status across all containing lists | +| ↳ `lists` | array | Lists containing this item | +| ↳ `id` | string | List UUID | +| ↳ `hasChildren` | boolean | Whether this item has children in the list | + +### `sap_concur_update_travel_request` + +Update a travel request (PUT /travelrequest/v4/requests/{requestUuid}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID to update | +| `body` | json | Yes | Fields to update on the travel request | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Updated travel request payload | +| ↳ `id` | string | Travel request UUID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `requestId` | string | Public-facing request ID \(4-6 alphanumeric characters\) | +| ↳ `name` | string | Request name | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `comment` | string | Last attached comment | +| ↳ `creationDate` | string | Creation timestamp | +| ↳ `lastModified` | string | Last modification timestamp | +| ↳ `submitDate` | string | Last submission timestamp | +| ↳ `startDate` | string | Trip start date \(ISO 8601\) | +| ↳ `endDate` | string | Trip end date \(ISO 8601\) | +| ↳ `startTime` | string | Trip start time \(HH:mm\) | +| ↳ `endTime` | string | Trip end time \(HH:mm\) | +| ↳ `approved` | boolean | Whether the request is approved | +| ↳ `pendingApproval` | boolean | Pending approval flag | +| ↳ `closed` | boolean | Closed flag | +| ↳ `everSentBack` | boolean | Ever-sent-back flag | +| ↳ `canceledPostApproval` | boolean | Canceled after approval flag | +| ↳ `approvalStatus` | json | Approval status | +| ↳ `code` | string | Status code \(NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK\) | +| ↳ `name` | string | Localized status name | +| ↳ `owner` | json | Travel request owner | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Owner first name | +| ↳ `lastName` | string | Owner last name | +| ↳ `approver` | json | Approver assigned to the request | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Approver first name | +| ↳ `lastName` | string | Approver last name | +| ↳ `policy` | json | Resource link to the applicable policy | +| ↳ `id` | string | Policy ID | +| ↳ `href` | string | Policy hyperlink | +| ↳ `type` | json | Request type | +| ↳ `code` | string | Request type code | +| ↳ `label` | string | Request type label | +| ↳ `mainDestination` | json | Main destination of the trip | +| ↳ `city` | string | City | +| ↳ `countryCode` | string | ISO country code | +| ↳ `countrySubDivisionCode` | string | ISO country sub-division code | +| ↳ `name` | string | Destination name | +| ↳ `totalApprovedAmount` | json | Total approved amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalPostedAmount` | json | Total posted amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalRemainingAmount` | json | Total remaining amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `operations` | array | Available workflow actions | + +### `sap_concur_update_user` + +Patch a user identity (PATCH /profile/identity/v4.1/Users/{id}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userUuid` | string | Yes | User UUID to update | +| `body` | json | Yes | SCIM PATCH operations payload \(\{ schemas, Operations: \[...\] \}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Updated SCIM User payload | + +### `sap_concur_upload_receipt_image` + +Upload an image-only receipt (POST /receipts/v4/users/{userId}/image-only-receipts). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who owns the receipt | +| `receipt` | json | Yes | Receipt image file \(UserFile reference\). Supported formats: PDF, PNG, JPEG, GIF, TIFF | +| `forwardId` | string | No | Optional client-supplied dedup id \(max 40 chars\). Sent as the concur-forwardid header. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Image-only receipt upload response \(HTTP 202 Accepted; Location and Link response headers exposed in body\) | +| ↳ `location` | string | Location header URL for the new receipt image \(e.g. /receipts/v4/images/\{receiptId\}\) | +| ↳ `link` | string | Link header URL pointing to /receipts/v4/status/\{receiptId\} | + + diff --git a/apps/docs/content/docs/en/tools/sap_s4hana.mdx b/apps/docs/content/docs/en/tools/sap_s4hana.mdx index 0c8aaf5c745..bc9bb49302a 100644 --- a/apps/docs/content/docs/en/tools/sap_s4hana.mdx +++ b/apps/docs/content/docs/en/tools/sap_s4hana.mdx @@ -62,10 +62,10 @@ List business partners from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessP | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -84,7 +84,22 @@ List business partners from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessP | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_BusinessPartner entities | +| `data` | json | OData v2 envelope `\{ d: \{ results: \[...\], __count?, __next? \} \}`. Properties listed below describe each element of `data.d.results`. | +| ↳ `BusinessPartner` | string | Business partner key \(up to 10 chars\) | +| ↳ `BusinessPartnerFullName` | string | Full name \(concatenated first/last or organization name\) | +| ↳ `BusinessPartnerCategory` | string | "1" Person, "2" Organization, "3" Group | +| ↳ `BusinessPartnerGrouping` | string | Grouping / number range \(tenant-configured\) | +| ↳ `BusinessPartnerType` | string | Business partner type \(tenant-configured\) | +| ↳ `BusinessPartnerUUID` | string | GUID identifier for the business partner | +| ↳ `BusinessPartnerIsBlocked` | boolean | Whether the business partner is centrally blocked | +| ↳ `FirstName` | string | First name \(Person\) | +| ↳ `LastName` | string | Last name \(Person\) | +| ↳ `OrganizationBPName1` | string | Organization name line 1 | +| ↳ `SearchTerm1` | string | Search term 1 | +| ↳ `CreationDate` | string | Date the partner was created \(OData /Date\(...\)/ literal\) | +| ↳ `CreatedByUser` | string | User who created the business partner | +| ↳ `LastChangeDate` | string | Date of last change \(OData /Date\(...\)/ literal\) | +| ↳ `LastChangedByUser` | string | User who last changed the business partner | ### `sap_s4hana_get_business_partner` @@ -94,10 +109,10 @@ Retrieve a single business partner by BusinessPartner key from SAP S/4HANA Cloud | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -113,7 +128,24 @@ Retrieve a single business partner by BusinessPartner key from SAP S/4HANA Cloud | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_BusinessPartner entity | +| `data` | json | A_BusinessPartner entity \(under d in OData v2\) | +| ↳ `BusinessPartner` | string | Business partner key \(up to 10 chars\) | +| ↳ `BusinessPartnerFullName` | string | Full name \(concatenated first/last or organization name\) | +| ↳ `BusinessPartnerCategory` | string | "1" Person, "2" Organization, "3" Group | +| ↳ `BusinessPartnerGrouping` | string | Grouping / number range \(tenant-configured\) | +| ↳ `BusinessPartnerType` | string | Business partner type \(tenant-configured\) | +| ↳ `BusinessPartnerUUID` | string | GUID identifier for the business partner | +| ↳ `BusinessPartnerIsBlocked` | boolean | Whether the business partner is centrally blocked | +| ↳ `FirstName` | string | First name \(Person\) | +| ↳ `LastName` | string | Last name \(Person\) | +| ↳ `OrganizationBPName1` | string | Organization name line 1 | +| ↳ `CorrespondenceLanguage` | string | Correspondence language \(2-char code, e.g. "EN"\) | +| ↳ `SearchTerm1` | string | Search term 1 | +| ↳ `SearchTerm2` | string | Search term 2 | +| ↳ `CreationDate` | string | Date the partner was created \(OData /Date\(...\)/ literal\) | +| ↳ `CreatedByUser` | string | User who created the business partner | +| ↳ `LastChangeDate` | string | Date of last change \(OData /Date\(...\)/ literal\) | +| ↳ `LastChangedByUser` | string | User who last changed the business partner | ### `sap_s4hana_create_business_partner` @@ -123,10 +155,10 @@ Create a business partner in SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Business | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -144,21 +176,33 @@ Create a business partner in SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Business | Parameter | Type | Description | | --------- | ---- | ----------- | -| `status` | number | HTTP status code returned by SAP | -| `data` | json | Created A_BusinessPartner entity | +| `status` | number | HTTP status code returned by SAP \(201 on success\) | +| `data` | json | Created A_BusinessPartner entity \(under d in OData v2\) | +| ↳ `BusinessPartner` | string | Generated business partner key \(up to 10 chars\) | +| ↳ `BusinessPartnerFullName` | string | Full name \(concatenated first/last or organization name\) | +| ↳ `BusinessPartnerCategory` | string | "1" Person, "2" Organization, "3" Group | +| ↳ `BusinessPartnerGrouping` | string | Grouping / number range used to assign the key | +| ↳ `BusinessPartnerType` | string | Business partner type \(tenant-configured\) | +| ↳ `BusinessPartnerUUID` | string | GUID identifier for the business partner | +| ↳ `FirstName` | string | First name \(Person\) | +| ↳ `LastName` | string | Last name \(Person\) | +| ↳ `OrganizationBPName1` | string | Organization name line 1 | +| ↳ `CreationDate` | string | Date the partner was created \(OData /Date\(...\)/ literal\) | +| ↳ `CreatedByUser` | string | User who created the business partner | +| ↳ `LastChangeDate` | string | Date of last change \(OData /Date\(...\)/ literal\) | ### `sap_s4hana_update_business_partner` -Update fields on an A_BusinessPartner entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. +Update fields on an A_BusinessPartner entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. Deep updates on nested associations (e.g. to_BusinessPartnerAddress) are not supported by SAP (KBA 2833338) — use the dedicated child endpoints. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -175,6 +219,15 @@ Update fields on an A_BusinessPartner entity in SAP S/4HANA Cloud (API_BUSINESS_ | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | | `data` | json | Null on 204 success, or updated A_BusinessPartner entity if SAP returns one | +| ↳ `BusinessPartner` | string | Business partner key | +| ↳ `BusinessPartnerFullName` | string | Full name \(concatenated first/last or organization name\) | +| ↳ `BusinessPartnerCategory` | string | "1" Person, "2" Organization, "3" Group | +| ↳ `BusinessPartnerGrouping` | string | Grouping / number range | +| ↳ `FirstName` | string | First name \(Person\) | +| ↳ `LastName` | string | Last name \(Person\) | +| ↳ `OrganizationBPName1` | string | Organization name line 1 | +| ↳ `LastChangeDate` | string | Date of last change \(OData /Date\(...\)/ literal\) | +| ↳ `LastChangedByUser` | string | User who last changed the business partner | ### `sap_s4hana_list_customers` @@ -184,10 +237,10 @@ List customers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Customer) with op | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -206,7 +259,30 @@ List customers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Customer) with op | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_Customer entities | +| `data` | json | Array of A_Customer entities, or `\{ results, __count?, __next? \}` when pagination metadata is present \(proxy unwraps the OData v2 `d` envelope\). Properties below describe each customer item. | +| ↳ `Customer` | string | Customer key \(up to 10 characters\) | +| ↳ `CustomerName` | string | Name of customer | +| ↳ `CustomerFullName` | string | Full name of the customer | +| ↳ `CustomerAccountGroup` | string | Customer account group | +| ↳ `CustomerClassification` | string | Customer classification code | +| ↳ `CustomerCorporateGroup` | string | Corporate group code | +| ↳ `AuthorizationGroup` | string | Authorization group | +| ↳ `Supplier` | string | Linked supplier account number | +| ↳ `FiscalAddress` | string | Fiscal address ID | +| ↳ `Industry` | string | Industry key | +| ↳ `NielsenRegion` | string | Nielsen ID | +| ↳ `ResponsibleType` | string | Responsible type | +| ↳ `NFPartnerIsNaturalPerson` | string | Natural person indicator | +| ↳ `InternationalLocationNumber1` | string | International location number 1 | +| ↳ `TaxNumberType` | string | Tax number type | +| ↳ `VATRegistration` | string | VAT registration number | +| ↳ `DeletionIndicator` | boolean | Central deletion flag | +| ↳ `OrderIsBlockedForCustomer` | string | Central order block reason code | +| ↳ `PostingIsBlocked` | boolean | Central posting block flag | +| ↳ `DeliveryIsBlocked` | string | Central delivery block reason code | +| ↳ `BillingIsBlockedForCustomer` | string | Central billing block reason code | +| ↳ `CreationDate` | string | Creation date \(OData v2 epoch\) | +| ↳ `CreatedByUser` | string | User who created the customer | ### `sap_s4hana_get_customer` @@ -216,10 +292,10 @@ Retrieve a single customer by Customer key from SAP S/4HANA Cloud (API_BUSINESS_ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -235,20 +311,43 @@ Retrieve a single customer by Customer key from SAP S/4HANA Cloud (API_BUSINESS_ | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_Customer entity | +| `data` | object | A_Customer entity | +| ↳ `Customer` | string | Customer key \(up to 10 characters\) | +| ↳ `CustomerName` | string | Name of customer | +| ↳ `CustomerFullName` | string | Full name of the customer | +| ↳ `CustomerAccountGroup` | string | Customer account group | +| ↳ `CustomerClassification` | string | Customer classification code | +| ↳ `CustomerCorporateGroup` | string | Corporate group code | +| ↳ `AuthorizationGroup` | string | Authorization group | +| ↳ `Supplier` | string | Linked supplier account number | +| ↳ `FiscalAddress` | string | Fiscal address ID | +| ↳ `Industry` | string | Industry key | +| ↳ `NielsenRegion` | string | Nielsen ID | +| ↳ `ResponsibleType` | string | Responsible type | +| ↳ `NFPartnerIsNaturalPerson` | string | Natural person indicator | +| ↳ `InternationalLocationNumber1` | string | International location number 1 | +| ↳ `TaxNumberType` | string | Tax number type | +| ↳ `VATRegistration` | string | VAT registration number | +| ↳ `DeletionIndicator` | boolean | Central deletion flag | +| ↳ `OrderIsBlockedForCustomer` | string | Central order block reason code | +| ↳ `PostingIsBlocked` | boolean | Central posting block flag | +| ↳ `DeliveryIsBlocked` | string | Central delivery block reason code | +| ↳ `BillingIsBlockedForCustomer` | string | Central billing block reason code | +| ↳ `CreationDate` | string | Creation date \(OData v2 epoch\) | +| ↳ `CreatedByUser` | string | User who created the customer | ### `sap_s4hana_update_customer` -Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. A_Customer PATCH is limited to modifiable fields such as OrderIsBlockedForCustomer, DeliveryIsBlock, BillingIsBlockedForCustomer, PostingIsBlocked, and DeletionIndicator. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. +Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. A_Customer is limited to modifiable fields such as OrderIsBlockedForCustomer, DeliveryIsBlocked, BillingIsBlockedForCustomer (Edm.String reason codes like #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -256,7 +355,7 @@ Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER | `username` | string | No | Username for HTTP Basic auth | | `password` | string | No | Password for HTTP Basic auth | | `customer` | string | Yes | Customer key to update \(string, up to 10 characters\) | -| `body` | json | Yes | JSON object with A_Customer fields to update \(e.g., \{"OrderIsBlockedForCustomer":true,"DeletionIndicator":false\}\) | +| `body` | json | Yes | JSON object with A_Customer fields to update \(e.g., \{"OrderIsBlockedForCustomer":"01","DeletionIndicator":false\}\). Block-reason fields are Edm.String codes, not booleans. | | `ifMatch` | string | No | If-Match ETag for optimistic concurrency. Defaults to "*" \(unconditional\). | #### Output @@ -264,7 +363,15 @@ Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on 204 success, or updated A_Customer entity if SAP returns one | +| `data` | object | Null on 204 success, or updated A_Customer entity if SAP returns one | +| ↳ `Customer` | string | Customer key \(up to 10 characters\) | +| ↳ `CustomerName` | string | Name of customer | +| ↳ `CustomerAccountGroup` | string | Customer account group | +| ↳ `DeletionIndicator` | boolean | Central deletion flag | +| ↳ `OrderIsBlockedForCustomer` | string | Central order block reason code | +| ↳ `PostingIsBlocked` | boolean | Central posting block flag | +| ↳ `DeliveryIsBlocked` | string | Central delivery block reason code | +| ↳ `BillingIsBlockedForCustomer` | string | Central billing block reason code | ### `sap_s4hana_list_suppliers` @@ -274,10 +381,10 @@ List suppliers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Supplier) with op | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -296,7 +403,47 @@ List suppliers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Supplier) with op | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_Supplier entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_Supplier entities | +| ↳ `Supplier` | string | Supplier key \(up to 10 characters\) | +| ↳ `AlternativePayeeAccountNumber` | string | Account number of the alternative payee | +| ↳ `AuthorizationGroup` | string | Authorization group | +| ↳ `BusinessPartner` | string | Linked BusinessPartner key | +| ↳ `BR_TaxIsSplit` | boolean | Brazil-specific tax split flag | +| ↳ `CreatedByUser` | string | User who created the supplier | +| ↳ `CreationDate` | string | Creation date \(OData v2 epoch\) | +| ↳ `Customer` | string | Linked customer key \(if any\) | +| ↳ `DeletionIndicator` | boolean | Central deletion flag | +| ↳ `BirthDate` | string | Date of birth \(OData v2 epoch\) | +| ↳ `ConcatenatedInternationalLocNo` | string | Concatenated international location number | +| ↳ `FiscalAddress` | string | Fiscal address number | +| ↳ `Industry` | string | Industry key | +| ↳ `InternationalLocationNumber1` | string | International location number, part 1 | +| ↳ `InternationalLocationNumber2` | string | International location number, part 2 | +| ↳ `InternationalLocationNumber3` | string | International location number, part 3 | +| ↳ `IsNaturalPerson` | boolean | Indicates whether the supplier is a natural person | +| ↳ `PaymentIsBlockedForSupplier` | boolean | Payment block flag | +| ↳ `PostingIsBlocked` | boolean | Posting block flag | +| ↳ `PurchasingIsBlocked` | boolean | Purchasing block flag | +| ↳ `ResponsibleType` | string | Type of business \(Brazil\) | +| ↳ `SupplierAccountGroup` | string | Supplier account group | +| ↳ `SupplierCorporateGroup` | string | Corporate group identifier | +| ↳ `SupplierFullName` | string | Full name of the supplier | +| ↳ `SupplierName` | string | Supplier name | +| ↳ `SupplierProcurementBlock` | string | Procurement block at supplier level | +| ↳ `SuplrProofOfDelivRlvtCode` | string | Proof of delivery relevance code | +| ↳ `SuplrQltyInProcmtCertfnValidTo` | string | Quality certification validity end date \(OData v2 epoch\) | +| ↳ `SuplrQualityManagementSystem` | string | Quality management system of the supplier | +| ↳ `TaxNumber1` | string | Tax number 1 | +| ↳ `TaxNumber2` | string | Tax number 2 | +| ↳ `TaxNumber3` | string | Tax number 3 | +| ↳ `TaxNumber4` | string | Tax number 4 | +| ↳ `TaxNumber5` | string | Tax number 5 | +| ↳ `TaxNumberResponsible` | string | Tax number of responsible party | +| ↳ `TaxNumberType` | string | Tax number type | +| ↳ `VATRegistration` | string | VAT registration number | +| ↳ `__next` | string | OData skiptoken URL for next page | ### `sap_s4hana_get_supplier` @@ -306,10 +453,10 @@ Retrieve a single supplier by Supplier key from SAP S/4HANA Cloud (API_BUSINESS_ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -325,20 +472,58 @@ Retrieve a single supplier by Supplier key from SAP S/4HANA Cloud (API_BUSINESS_ | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_Supplier entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_Supplier entity | +| ↳ `Supplier` | string | Supplier key \(up to 10 characters\) | +| ↳ `AlternativePayeeAccountNumber` | string | Account number of the alternative payee | +| ↳ `AuthorizationGroup` | string | Authorization group | +| ↳ `BusinessPartner` | string | Linked BusinessPartner key | +| ↳ `BR_TaxIsSplit` | boolean | Brazil-specific tax split flag | +| ↳ `CreatedByUser` | string | User who created the supplier | +| ↳ `CreationDate` | string | Creation date \(OData v2 epoch\) | +| ↳ `Customer` | string | Linked customer key \(if any\) | +| ↳ `DeletionIndicator` | boolean | Central deletion flag | +| ↳ `BirthDate` | string | Date of birth \(OData v2 epoch\) | +| ↳ `ConcatenatedInternationalLocNo` | string | Concatenated international location number | +| ↳ `FiscalAddress` | string | Fiscal address number | +| ↳ `Industry` | string | Industry key | +| ↳ `InternationalLocationNumber1` | string | International location number, part 1 | +| ↳ `InternationalLocationNumber2` | string | International location number, part 2 | +| ↳ `InternationalLocationNumber3` | string | International location number, part 3 | +| ↳ `IsNaturalPerson` | boolean | Indicates whether the supplier is a natural person | +| ↳ `PaymentIsBlockedForSupplier` | boolean | Payment block flag | +| ↳ `PostingIsBlocked` | boolean | Posting block flag | +| ↳ `PurchasingIsBlocked` | boolean | Purchasing block flag | +| ↳ `ResponsibleType` | string | Type of business \(Brazil\) | +| ↳ `SupplierAccountGroup` | string | Supplier account group | +| ↳ `SupplierCorporateGroup` | string | Corporate group identifier | +| ↳ `SupplierFullName` | string | Full name of the supplier | +| ↳ `SupplierName` | string | Supplier name | +| ↳ `SupplierProcurementBlock` | string | Procurement block at supplier level | +| ↳ `SuplrProofOfDelivRlvtCode` | string | Proof of delivery relevance code | +| ↳ `SuplrQltyInProcmtCertfnValidTo` | string | Quality certification validity end date \(OData v2 epoch\) | +| ↳ `SuplrQualityManagementSystem` | string | Quality management system of the supplier | +| ↳ `TaxNumber1` | string | Tax number 1 | +| ↳ `TaxNumber2` | string | Tax number 2 | +| ↳ `TaxNumber3` | string | Tax number 3 | +| ↳ `TaxNumber4` | string | Tax number 4 | +| ↳ `TaxNumber5` | string | Tax number 5 | +| ↳ `TaxNumberResponsible` | string | Tax number of responsible party | +| ↳ `TaxNumberType` | string | Tax number type | +| ↳ `VATRegistration` | string | VAT registration number | ### `sap_s4hana_update_supplier` -Update fields on an A_Supplier entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. A_Supplier PATCH is limited to modifiable fields such as PostingIsBlocked, PurchasingIsBlocked, PaymentIsBlockedForSupplier, DeletionIndicator, and SupplierAccountGroup. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. +Update fields on an A_Supplier entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. A_Supplier is limited to modifiable fields such as PostingIsBlocked, PurchasingIsBlocked, PaymentIsBlockedForSupplier, DeletionIndicator, and SupplierAccountGroup; company-code/purchasing-org segments must be updated via the `to_SupplierCompany` / `to_SupplierPurchasingOrg` deep-update endpoints. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -354,7 +539,16 @@ Update fields on an A_Supplier entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on 204 success, or updated A_Supplier entity if SAP returns one | +| `data` | json | Null on 204 success, or OData v2 envelope with updated entity at output.data.d when SAP returns a representation | +| ↳ `d` | json | A_Supplier entity \(when SAP returns a representation\) | +| ↳ `Supplier` | string | Supplier key \(up to 10 characters\) | +| ↳ `SupplierName` | string | Supplier name | +| ↳ `SupplierAccountGroup` | string | Supplier account group | +| ↳ `BusinessPartner` | string | Linked BusinessPartner key | +| ↳ `PaymentIsBlockedForSupplier` | boolean | Payment block flag | +| ↳ `PostingIsBlocked` | boolean | Posting block flag | +| ↳ `PurchasingIsBlocked` | boolean | Purchasing block flag | +| ↳ `DeletionIndicator` | boolean | Central deletion flag | ### `sap_s4hana_list_sales_orders` @@ -364,10 +558,10 @@ List sales orders from SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) wit | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -386,7 +580,26 @@ List sales orders from SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) wit | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_SalesOrder entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_SalesOrder entities | +| ↳ `SalesOrder` | string | Sales order number | +| ↳ `SalesOrderType` | string | Sales document type \(e.g., OR\) | +| ↳ `SalesOrganization` | string | Sales organization | +| ↳ `DistributionChannel` | string | Distribution channel | +| ↳ `OrganizationDivision` | string | Division | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `TotalNetAmount` | string | Total net amount | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `SalesOrderDate` | string | Sales order date \(OData /Date\(ms\)/\) | +| ↳ `RequestedDeliveryDate` | string | Requested delivery date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDate` | string | Last change date \(OData /Date\(ms\)/\) | +| ↳ `PurchaseOrderByCustomer` | string | Customer purchase order reference | +| ↳ `OverallSDProcessStatus` | string | Overall sales document process status | +| ↳ `OverallTotalDeliveryStatus` | string | Overall total delivery status | +| ↳ `OverallSDDocumentRejectionSts` | string | Overall sales document rejection status | +| ↳ `__next` | string | OData skiptoken URL for next page | ### `sap_s4hana_get_sales_order` @@ -396,10 +609,10 @@ Retrieve a single sales order by SalesOrder key from SAP S/4HANA Cloud (API_SALE | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -415,7 +628,27 @@ Retrieve a single sales order by SalesOrder key from SAP S/4HANA Cloud (API_SALE | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_SalesOrder entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_SalesOrder entity | +| ↳ `SalesOrder` | string | Sales order number | +| ↳ `SalesOrderType` | string | Sales document type | +| ↳ `SalesOrganization` | string | Sales organization | +| ↳ `DistributionChannel` | string | Distribution channel | +| ↳ `OrganizationDivision` | string | Division | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `PurchaseOrderByCustomer` | string | Customer purchase order reference | +| ↳ `SalesOrderDate` | string | Sales order date \(OData /Date\(ms\)/\) | +| ↳ `RequestedDeliveryDate` | string | Requested delivery date \(OData /Date\(ms\)/\) | +| ↳ `PricingDate` | string | Pricing date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDate` | string | Last change date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDateTime` | string | Last change timestamp \(OData /Date\(ms\)/\) | +| ↳ `TotalNetAmount` | string | Total net amount | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `CreationDate` | string | Creation date | +| ↳ `OverallSDProcessStatus` | string | Overall sales document process status | +| ↳ `OverallTotalDeliveryStatus` | string | Overall total delivery status | +| ↳ `OverallSDDocumentRejectionSts` | string | Overall sales document rejection status | +| ↳ `to_Item` | json | Sales order items \(when $expand=to_Item\) | ### `sap_s4hana_create_sales_order` @@ -425,10 +658,10 @@ Create a sales order in SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) wi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -447,21 +680,34 @@ Create a sales order in SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) wi | Parameter | Type | Description | | --------- | ---- | ----------- | -| `status` | number | HTTP status code returned by SAP | -| `data` | json | Created A_SalesOrder entity \(with deep-inserted items if expanded by SAP\) | +| `status` | number | HTTP status code returned by SAP \(201 on create\) | +| `data` | json | OData v2 response envelope; created entity at output.data.d | +| ↳ `d` | json | Created A_SalesOrder entity | +| ↳ `SalesOrder` | string | Newly assigned sales order number | +| ↳ `SalesOrderType` | string | Sales document type | +| ↳ `SalesOrganization` | string | Sales organization | +| ↳ `DistributionChannel` | string | Distribution channel | +| ↳ `OrganizationDivision` | string | Division | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `TotalNetAmount` | string | Total net amount | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `CreationDate` | string | Creation date | +| ↳ `OverallSDProcessStatus` | string | Overall sales document process status | +| ↳ `OverallTotalDeliveryStatus` | string | Overall total delivery status | +| ↳ `to_Item` | json | Deep-inserted sales order items as returned by SAP | ### `sap_s4hana_update_sales_order` -Update fields on an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. +Update fields on an A_SalesOrder header in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. Header-only — deep updates to to_Item / to_Partner / to_PricingElement navigations are not supported (see SAP KBA 2833338); use A_SalesOrderItem operations for line-level changes. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -477,7 +723,13 @@ Update fields on an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SR | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on 204 success, or updated A_SalesOrder entity if SAP returns one | +| `data` | json | Null on 204 success; otherwise OData v2 envelope with the updated entity at output.data.d | +| ↳ `d` | json | Updated A_SalesOrder entity \(when SAP returns one\) | +| ↳ `SalesOrder` | string | Sales order number | +| ↳ `SalesOrderType` | string | Sales document type | +| ↳ `PurchaseOrderByCustomer` | string | Customer purchase order reference | +| ↳ `OverallSDProcessStatus` | string | Overall sales document process status | +| ↳ `OverallTotalDeliveryStatus` | string | Overall total delivery status | ### `sap_s4hana_delete_sales_order` @@ -487,10 +739,10 @@ Delete an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). Only o | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -505,7 +757,7 @@ Delete an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). Only o | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on successful deletion | +| `data` | json | Null on successful deletion \(SAP returns 204 No Content\) | ### `sap_s4hana_list_outbound_deliveries` @@ -515,10 +767,10 @@ List outbound deliveries from SAP S/4HANA Cloud (API_OUTBOUND_DELIVERY_SRV;v=000 | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -537,7 +789,27 @@ List outbound deliveries from SAP S/4HANA Cloud (API_OUTBOUND_DELIVERY_SRV;v=000 | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_OutbDeliveryHeader entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_OutbDeliveryHeader entities | +| ↳ `DeliveryDocument` | string | Outbound delivery number | +| ↳ `DeliveryDocumentType` | string | Delivery document type \(e.g., LF\) | +| ↳ `SDDocumentCategory` | string | SD document category \(e.g., J = outbound delivery\) | +| ↳ `ShippingPoint` | string | Shipping point | +| ↳ `ShippingType` | string | Shipping type | +| ↳ `ShipToParty` | string | Ship-to business partner | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `DeliveryDate` | string | Delivery date \(Edm.DateTime\) | +| ↳ `ActualGoodsMovementDate` | string | Actual goods issue date \(Edm.DateTime\) | +| ↳ `PlannedGoodsIssueDate` | string | Planned goods issue date \(Edm.DateTime\) | +| ↳ `OverallSDProcessStatus` | string | Overall SD process \(delivery\) status | +| ↳ `OverallGoodsMovementStatus` | string | Overall goods movement status | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `DocumentDate` | string | Document date \(Edm.DateTime\) | +| ↳ `CreationDate` | string | Creation date \(Edm.DateTime\) | +| ↳ `LastChangeDate` | string | Last change date \(Edm.DateTime\) | +| ↳ `__next` | string | OData skiptoken URL for next page | +| ↳ `__count` | string | Total count when $inlinecount=allpages is used | ### `sap_s4hana_get_outbound_delivery` @@ -547,10 +819,10 @@ Retrieve a single outbound delivery by DeliveryDocument key from SAP S/4HANA Clo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -566,7 +838,26 @@ Retrieve a single outbound delivery by DeliveryDocument key from SAP S/4HANA Clo | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_OutbDeliveryHeader entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_OutbDeliveryHeader entity | +| ↳ `DeliveryDocument` | string | Outbound delivery number | +| ↳ `DeliveryDocumentType` | string | Delivery document type | +| ↳ `SDDocumentCategory` | string | SD document category \(e.g., J = outbound delivery\) | +| ↳ `ShippingPoint` | string | Shipping point | +| ↳ `ShippingType` | string | Shipping type | +| ↳ `ShipToParty` | string | Ship-to business partner | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `DeliveryDate` | string | Delivery date \(Edm.DateTime\) | +| ↳ `ActualGoodsMovementDate` | string | Actual goods issue date \(Edm.DateTime\) | +| ↳ `PlannedGoodsIssueDate` | string | Planned goods issue date \(Edm.DateTime\) | +| ↳ `OverallSDProcessStatus` | string | Overall SD process \(delivery\) status | +| ↳ `OverallGoodsMovementStatus` | string | Overall goods movement status | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `DocumentDate` | string | Document date \(Edm.DateTime\) | +| ↳ `CreationDate` | string | Creation date \(Edm.DateTime\) | +| ↳ `LastChangeDate` | string | Last change date \(Edm.DateTime\) | +| ↳ `to_DeliveryDocumentItem` | json | Delivery items \(when $expand=to_DeliveryDocumentItem\) | +| ↳ `to_DeliveryDocumentPartner` | json | Delivery partners \(when $expand=to_DeliveryDocumentPartner\) | ### `sap_s4hana_list_inbound_deliveries` @@ -576,10 +867,10 @@ List inbound deliveries from SAP S/4HANA Cloud (API_INBOUND_DELIVERY_SRV;v=0002, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -598,7 +889,25 @@ List inbound deliveries from SAP S/4HANA Cloud (API_INBOUND_DELIVERY_SRV;v=0002, | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_InbDeliveryHeader entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_InbDeliveryHeader entities | +| ↳ `DeliveryDocument` | string | Inbound delivery number | +| ↳ `DeliveryDocumentType` | string | Delivery document type \(e.g., EL\) | +| ↳ `SDDocumentCategory` | string | SD document category \(e.g., 7 = inbound delivery\) | +| ↳ `ReceivingPlant` | string | Receiving plant | +| ↳ `Supplier` | string | Supplier business partner | +| ↳ `ShipToParty` | string | Ship-to business partner | +| ↳ `DeliveryDate` | string | Delivery date \(Edm.DateTime\) | +| ↳ `ActualGoodsMovementDate` | string | Actual goods movement \(receipt\) date \(Edm.DateTime\) | +| ↳ `PlannedGoodsMovementDate` | string | Planned goods movement date \(Edm.DateTime\) | +| ↳ `OverallSDProcessStatus` | string | Overall SD process \(delivery\) status | +| ↳ `OverallGoodsMovementStatus` | string | Overall goods movement status | +| ↳ `DocumentDate` | string | Document date \(Edm.DateTime\) | +| ↳ `CreationDate` | string | Creation date \(Edm.DateTime\) | +| ↳ `LastChangeDate` | string | Last change date \(Edm.DateTime\) | +| ↳ `__next` | string | OData skiptoken URL for next page | +| ↳ `__count` | string | Total count when $inlinecount=allpages is used | ### `sap_s4hana_get_inbound_delivery` @@ -608,10 +917,10 @@ Retrieve a single inbound delivery by DeliveryDocument key from SAP S/4HANA Clou | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -627,7 +936,24 @@ Retrieve a single inbound delivery by DeliveryDocument key from SAP S/4HANA Clou | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_InbDeliveryHeader entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_InbDeliveryHeader entity | +| ↳ `DeliveryDocument` | string | Inbound delivery number | +| ↳ `DeliveryDocumentType` | string | Delivery document type | +| ↳ `SDDocumentCategory` | string | SD document category \(e.g., 7 = inbound delivery\) | +| ↳ `ReceivingPlant` | string | Receiving plant | +| ↳ `Supplier` | string | Supplier business partner | +| ↳ `ShipToParty` | string | Ship-to business partner | +| ↳ `DeliveryDate` | string | Delivery date \(Edm.DateTime\) | +| ↳ `ActualGoodsMovementDate` | string | Actual goods movement \(receipt\) date \(Edm.DateTime\) | +| ↳ `PlannedGoodsMovementDate` | string | Planned goods movement date \(Edm.DateTime\) | +| ↳ `OverallSDProcessStatus` | string | Overall SD process \(delivery\) status | +| ↳ `OverallGoodsMovementStatus` | string | Overall goods movement status | +| ↳ `DocumentDate` | string | Document date \(Edm.DateTime\) | +| ↳ `CreationDate` | string | Creation date \(Edm.DateTime\) | +| ↳ `LastChangeDate` | string | Last change date \(Edm.DateTime\) | +| ↳ `to_DeliveryDocumentItem` | json | Delivery items \(when $expand=to_DeliveryDocumentItem\) | +| ↳ `to_DeliveryDocumentPartner` | json | Delivery partners \(when $expand=to_DeliveryDocumentPartner\) | ### `sap_s4hana_list_billing_documents` @@ -637,10 +963,10 @@ List billing documents (customer invoices) from SAP S/4HANA Cloud (API_BILLING_D | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -652,14 +978,47 @@ List billing documents (customer invoices) from SAP S/4HANA Cloud (API_BILLING_D | `skip` | number | No | Number of results to skip \($skip\) | | `orderBy` | string | No | OData $orderby expression | | `select` | string | No | Comma-separated fields to return \($select\) | -| `expand` | string | No | Comma-separated navigation properties to expand \(e.g., "to_Item,to_Partner"\) | +| `expand` | string | No | Comma-separated navigation properties to expand \(e.g., "to_Item,to_Partner,to_PricingElement"\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_BillingDocument entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_BillingDocument entities | +| ↳ `BillingDocument` | string | Billing document number | +| ↳ `SDDocumentCategory` | string | SD document category | +| ↳ `BillingDocumentCategory` | string | Billing document category | +| ↳ `BillingDocumentType` | string | Billing document type \(e.g., F2\) | +| ↳ `BillingDocumentDate` | string | Billing document date \(OData /Date\(ms\)/\) | +| ↳ `BillingDocumentIsCancelled` | boolean | Whether the billing document is cancelled | +| ↳ `CancelledBillingDocument` | string | Cancelled billing document number | +| ↳ `TotalNetAmount` | string | Total net amount \(Edm.Decimal as string\) | +| ↳ `TaxAmount` | string | Tax amount \(Edm.Decimal as string\) | +| ↳ `TotalGrossAmount` | string | Total gross amount \(Edm.Decimal as string\) | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `PayerParty` | string | Payer party | +| ↳ `SalesOrganization` | string | Sales organization | +| ↳ `DistributionChannel` | string | Distribution channel | +| ↳ `Division` | string | Division | +| ↳ `CompanyCode` | string | Company code | +| ↳ `FiscalYear` | string | Fiscal year | +| ↳ `OverallBillingStatus` | string | Overall billing status | +| ↳ `AccountingPostingStatus` | string | Accounting posting status | +| ↳ `AccountingTransferStatus` | string | Accounting transfer status | +| ↳ `InvoiceClearingStatus` | string | Invoice clearing status | +| ↳ `AccountingDocument` | string | Linked accounting document | +| ↳ `CustomerPaymentTerms` | string | Customer payment terms | +| ↳ `PaymentMethod` | string | Payment method | +| ↳ `DocumentReferenceID` | string | Document reference ID | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDate` | string | Last change date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDateTime` | string | Last change date-time \(Edm.DateTimeOffset\) | +| ↳ `__next` | string | OData skiptoken URL for next page | +| ↳ `__count` | string | Total count when $inlinecount=allpages is used | ### `sap_s4hana_get_billing_document` @@ -669,10 +1028,10 @@ Retrieve a single billing document (customer invoice) by BillingDocument key fro | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -681,14 +1040,47 @@ Retrieve a single billing document (customer invoice) by BillingDocument key fro | `password` | string | No | Password for HTTP Basic auth | | `billingDocument` | string | Yes | BillingDocument key \(string, up to 10 characters\) | | `select` | string | No | Comma-separated fields to return \($select\) | -| `expand` | string | No | Comma-separated navigation properties to expand \(e.g., "to_Item,to_Partner"\) | +| `expand` | string | No | Comma-separated navigation properties to expand \(e.g., "to_Item,to_Partner,to_PricingElement"\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_BillingDocument entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_BillingDocument entity | +| ↳ `BillingDocument` | string | Billing document number | +| ↳ `SDDocumentCategory` | string | SD document category | +| ↳ `BillingDocumentCategory` | string | Billing document category | +| ↳ `BillingDocumentType` | string | Billing document type | +| ↳ `BillingDocumentDate` | string | Billing document date \(OData /Date\(ms\)/\) | +| ↳ `BillingDocumentIsCancelled` | boolean | Whether the billing document is cancelled | +| ↳ `CancelledBillingDocument` | string | Cancelled billing document number | +| ↳ `TotalNetAmount` | string | Total net amount \(Edm.Decimal as string\) | +| ↳ `TaxAmount` | string | Tax amount \(Edm.Decimal as string\) | +| ↳ `TotalGrossAmount` | string | Total gross amount \(Edm.Decimal as string\) | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `PayerParty` | string | Payer party | +| ↳ `SalesOrganization` | string | Sales organization | +| ↳ `DistributionChannel` | string | Distribution channel | +| ↳ `Division` | string | Division | +| ↳ `CompanyCode` | string | Company code | +| ↳ `FiscalYear` | string | Fiscal year | +| ↳ `OverallBillingStatus` | string | Overall billing status | +| ↳ `AccountingPostingStatus` | string | Accounting posting status | +| ↳ `AccountingTransferStatus` | string | Accounting transfer status | +| ↳ `InvoiceClearingStatus` | string | Invoice clearing status | +| ↳ `AccountingDocument` | string | Linked accounting document | +| ↳ `CustomerPaymentTerms` | string | Customer payment terms | +| ↳ `PaymentMethod` | string | Payment method | +| ↳ `DocumentReferenceID` | string | Document reference ID | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDate` | string | Last change date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDateTime` | string | Last change date-time \(Edm.DateTimeOffset\) | +| ↳ `to_Item` | json | Billing document items \(when $expand=to_Item\) | +| ↳ `to_Partner` | json | Billing document partners \(when $expand=to_Partner\) | +| ↳ `to_PricingElement` | json | Billing document pricing elements \(when $expand=to_PricingElement\) | ### `sap_s4hana_list_products` @@ -698,10 +1090,10 @@ List products (materials) from SAP S/4HANA Cloud (API_PRODUCT_SRV, A_Product) wi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -720,7 +1112,30 @@ List products (materials) from SAP S/4HANA Cloud (API_PRODUCT_SRV, A_Product) wi | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_Product entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_Product entities | +| ↳ `Product` | string | Product \(material\) number | +| ↳ `ProductType` | string | Product type \(e.g., FERT, HAWA\) | +| ↳ `ProductGroup` | string | Material group | +| ↳ `BaseUnit` | string | Base unit of measure | +| ↳ `Brand` | string | Brand | +| ↳ `Division` | string | Division | +| ↳ `GrossWeight` | string | Gross weight | +| ↳ `NetWeight` | string | Net weight | +| ↳ `WeightUnit` | string | Weight unit of measure | +| ↳ `CrossPlantStatus` | string | Cross-plant material status | +| ↳ `IsMarkedForDeletion` | boolean | Deletion flag | +| ↳ `ProductStandardID` | string | Standard product ID \(e.g., GTIN\) | +| ↳ `ItemCategoryGroup` | string | Item category group | +| ↳ `ProductOldID` | string | Legacy/old product ID | +| ↳ `CreatedByUser` | string | User who created the product | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `LastChangedByUser` | string | User who last changed the product | +| ↳ `LastChangeDate` | string | Last change date | +| ↳ `LastChangeDateTime` | string | Last change timestamp \(Edm.DateTimeOffset\) | +| ↳ `__next` | string | OData skiptoken URL for next page | +| ↳ `__count` | string | Total count when $inlinecount=allpages is used | ### `sap_s4hana_get_product` @@ -730,10 +1145,10 @@ Retrieve a single product (material) by Product key from SAP S/4HANA Cloud (API_ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -749,20 +1164,43 @@ Retrieve a single product (material) by Product key from SAP S/4HANA Cloud (API_ | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_Product entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_Product entity | +| ↳ `Product` | string | Product \(material\) number | +| ↳ `ProductType` | string | Product type \(e.g., FERT, HAWA\) | +| ↳ `ProductGroup` | string | Material group | +| ↳ `BaseUnit` | string | Base unit of measure | +| ↳ `Brand` | string | Brand | +| ↳ `Division` | string | Division | +| ↳ `GrossWeight` | string | Gross weight | +| ↳ `NetWeight` | string | Net weight | +| ↳ `WeightUnit` | string | Weight unit of measure | +| ↳ `CrossPlantStatus` | string | Cross-plant material status | +| ↳ `IsMarkedForDeletion` | boolean | Deletion flag | +| ↳ `ProductStandardID` | string | Standard product ID \(e.g., GTIN\) | +| ↳ `ItemCategoryGroup` | string | Item category group | +| ↳ `ProductOldID` | string | Legacy/old product ID | +| ↳ `CreatedByUser` | string | User who created the product | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `LastChangedByUser` | string | User who last changed the product | +| ↳ `LastChangeDate` | string | Last change date | +| ↳ `LastChangeDateTime` | string | Last change timestamp \(Edm.DateTimeOffset\) | +| ↳ `to_Description` | json | Product descriptions \(when $expand=to_Description\) | +| ↳ `to_Plant` | json | Plant-level data \(when $expand=to_Plant\) | +| ↳ `to_ProductSales` | json | Sales data \(when $expand=to_ProductSales\) | ### `sap_s4hana_update_product` -Update fields on an A_Product entity in SAP S/4HANA Cloud (API_PRODUCT_SRV). PATCH only sends the fields you provide; existing values are preserved. Flat scalar header fields only — deep/multi-entity updates across navigation properties are not supported by API_PRODUCT_SRV PATCH/PUT (see SAP KBA 2833338); update child entities (plant, valuation, sales data, etc.) via their own endpoints. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET. +Update fields on an A_Product entity in SAP S/4HANA Cloud (API_PRODUCT_SRV). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. Flat scalar header fields only — deep/multi-entity updates across navigation properties are not supported by API_PRODUCT_SRV MERGE/PUT (see SAP KBA 2833338); update child entities (plant, valuation, sales data, etc.) via their own endpoints. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -778,7 +1216,14 @@ Update fields on an A_Product entity in SAP S/4HANA Cloud (API_PRODUCT_SRV). PAT | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on 204 success, or updated A_Product entity if SAP returns one | +| `data` | json | Null on 204 success, or OData v2 envelope with the updated A_Product entity at output.data.d | +| ↳ `d` | json | Updated A_Product entity \(only present if SAP returns a body\) | +| ↳ `Product` | string | Product \(material\) number | +| ↳ `ProductType` | string | Product type | +| ↳ `ProductGroup` | string | Material group | +| ↳ `BaseUnit` | string | Base unit of measure | +| ↳ `IsMarkedForDeletion` | boolean | Deletion flag | +| ↳ `LastChangeDate` | string | Last change date | ### `sap_s4hana_list_material_stock` @@ -788,10 +1233,10 @@ List material stock quantities from SAP S/4HANA Cloud (API_MATERIAL_STOCK_SRV, A | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -810,7 +1255,20 @@ List material stock quantities from SAP S/4HANA Cloud (API_MATERIAL_STOCK_SRV, A | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_MatlStkInAcctMod stock entries | +| `data` | json | OData payload containing the array of A_MatlStkInAcctMod stock entries | +| ↳ `Material` | string | Material number | +| ↳ `Plant` | string | Plant identifier | +| ↳ `StorageLocation` | string | Storage location identifier | +| ↳ `Batch` | string | Batch identifier | +| ↳ `Supplier` | string | Supplier business partner key | +| ↳ `Customer` | string | Customer business partner key | +| ↳ `WBSElementInternalID` | string | WBS element internal ID | +| ↳ `SDDocument` | string | SD document number | +| ↳ `SDDocumentItem` | string | SD document item | +| ↳ `InventorySpecialStockType` | string | Special stock type indicator | +| ↳ `InventoryStockType` | string | Stock type \(e.g., 01 unrestricted-use, 02 quality inspection, 03 blocked, 04 restricted-use\) | +| ↳ `MatlWrhsStkQtyInMatlBaseUnit` | string | Material warehouse stock quantity in material base unit \(Edm.Decimal serialized as string\) | +| ↳ `MaterialBaseUnit` | string | Material base unit of measure | ### `sap_s4hana_list_material_documents` @@ -820,10 +1278,10 @@ List material document headers (goods movements) from SAP S/4HANA Cloud (API_MAT | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -842,7 +1300,22 @@ List material document headers (goods movements) from SAP S/4HANA Cloud (API_MAT | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_MaterialDocumentHeader entities | +| `data` | json | OData payload containing the array of A_MaterialDocumentHeader entities | +| ↳ `MaterialDocumentYear` | string | Material document year \(4-digit fiscal year\) | +| ↳ `MaterialDocument` | string | Material document number | +| ↳ `DocumentDate` | string | Document date \(OData /Date\(...\)/ string\) | +| ↳ `PostingDate` | string | Posting date \(OData /Date\(...\)/ string\) | +| ↳ `MaterialDocumentHeaderText` | string | Header text describing the material document | +| ↳ `ReferenceDocument` | string | Reference document number | +| ↳ `GoodsMovementCode` | string | Goods movement code \(e.g., 01 GR for PO, 03 GI to cost center\) | +| ↳ `InventoryTransactionType` | string | Inventory transaction type indicator | +| ↳ `CreatedByUser` | string | User who created the material document | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(...\)/ string\) | +| ↳ `CreationTime` | string | Creation time \(OData PT...S string\) | +| ↳ `VersionForPrintingSlip` | string | Version for printing the goods movement slip | +| ↳ `ManualPrintIsTriggered` | boolean | Indicates whether manual print was triggered for this document | +| ↳ `CtrlPostgForExtWhseMgmtSyst` | string | Control posting for external warehouse management system | +| ↳ `to_MaterialDocumentItem` | json | Material document items \(only present when $expand=to_MaterialDocumentItem is supplied\) | ### `sap_s4hana_get_material_document` @@ -852,10 +1325,10 @@ Retrieve a single material document header by composite key (MaterialDocument + | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -872,7 +1345,22 @@ Retrieve a single material document header by composite key (MaterialDocument + | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_MaterialDocumentHeader entity | +| `data` | json | OData payload containing the A_MaterialDocumentHeader entity \(and optionally to_MaterialDocumentItem when expanded\) | +| ↳ `MaterialDocumentYear` | string | Material document year \(4-digit fiscal year\) | +| ↳ `MaterialDocument` | string | Material document number | +| ↳ `DocumentDate` | string | Document date \(OData /Date\(...\)/ string\) | +| ↳ `PostingDate` | string | Posting date \(OData /Date\(...\)/ string\) | +| ↳ `MaterialDocumentHeaderText` | string | Header text describing the material document | +| ↳ `ReferenceDocument` | string | Reference document number | +| ↳ `GoodsMovementCode` | string | Goods movement code \(e.g., 01 GR for PO, 03 GI to cost center\) | +| ↳ `InventoryTransactionType` | string | Inventory transaction type indicator | +| ↳ `CreatedByUser` | string | User who created the material document | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(...\)/ string\) | +| ↳ `CreationTime` | string | Creation time \(OData PT...S string\) | +| ↳ `VersionForPrintingSlip` | string | Version for printing the goods movement slip | +| ↳ `ManualPrintIsTriggered` | boolean | Indicates whether manual print was triggered for this document | +| ↳ `CtrlPostgForExtWhseMgmtSyst` | string | Control posting for external warehouse management system | +| ↳ `to_MaterialDocumentItem` | json | Material document items \(only present when $expand=to_MaterialDocumentItem is supplied\) | ### `sap_s4hana_list_purchase_requisitions` @@ -882,10 +1370,10 @@ List purchase requisitions from SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -904,7 +1392,14 @@ List purchase requisitions from SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_PurchaseRequisitionHeader entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_PurchaseRequisitionHeader entities | +| ↳ `PurchaseRequisition` | string | Purchase requisition number | +| ↳ `PurchaseRequisitionType` | string | Purchase requisition document type \(e.g., NB\) | +| ↳ `PurReqnDescription` | string | Purchase requisition description | +| ↳ `SourceDetermination` | string | Source-of-supply determination flag | +| ↳ `__next` | string | OData skiptoken URL for next page | ### `sap_s4hana_get_purchase_requisition` @@ -914,10 +1409,10 @@ Retrieve a single purchase requisition by PurchaseRequisition key from SAP S/4HA | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -933,7 +1428,13 @@ Retrieve a single purchase requisition by PurchaseRequisition key from SAP S/4HA | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_PurchaseRequisitionHeader entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_PurchaseRequisitionHeader entity | +| ↳ `PurchaseRequisition` | string | Purchase requisition number | +| ↳ `PurchaseRequisitionType` | string | PR document type \(e.g., NB\) | +| ↳ `PurReqnDescription` | string | Purchase requisition description | +| ↳ `SourceDetermination` | string | Source-of-supply determination flag | +| ↳ `to_PurchaseReqnItem` | json | Expanded PR items \(when $expand=to_PurchaseReqnItem\) | ### `sap_s4hana_create_purchase_requisition` @@ -943,10 +1444,10 @@ Create a purchase requisition in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -955,27 +1456,33 @@ Create a purchase requisition in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, | `password` | string | No | Password for HTTP Basic auth | | `purchaseRequisitionType` | string | Yes | PurchaseRequisitionType \(e.g., "NB" Standard PR\) | | `items` | json | Yes | to_PurchaseReqnItem deep-insert array \(e.g., \[\{"PurchaseRequisitionItem":"10","Material":"TG11","RequestedQuantity":"5","Plant":"1010","BaseUnit":"PC","DeliveryDate":"/Date\(1735689600000\)/"\}\]\) | -| `body` | json | No | Additional A_PurchaseRequisitionHeader fields merged into the create payload \(e.g., \{"PurchaseRequisitionDescription":"Office supplies"\}\) | +| `body` | json | No | Additional A_PurchaseRequisitionHeader fields merged into the create payload \(e.g., \{"PurReqnDescription":"Office supplies"\}\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Created A_PurchaseRequisitionHeader entity | +| `data` | json | OData v2 response envelope; created entity at output.data.d | +| ↳ `d` | json | Created A_PurchaseRequisitionHeader entity | +| ↳ `PurchaseRequisition` | string | Auto-assigned purchase requisition number | +| ↳ `PurchaseRequisitionType` | string | PR document type \(e.g., NB\) | +| ↳ `PurReqnDescription` | string | Purchase requisition description | +| ↳ `SourceDetermination` | string | Source-of-supply determination flag | +| ↳ `to_PurchaseReqnItem` | json | Created PR items returned in deep insert | ### `sap_s4hana_update_purchase_requisition` -Update fields on an A_PurchaseRequisitionHeader entity in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV; deprecated since S/4HANA 2402, successor is API_PURCHASEREQUISITION_2 OData v4). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. +Update fields on an A_PurchaseRequisitionHeader entity in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV; deprecated since S/4HANA 2402, successor is API_PURCHASEREQUISITION_2 OData v4). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. Header-only — deep updates across navigations are not supported (SAP KBA 2833338); use the A_PurchaseReqnItem entity directly to modify items. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -991,7 +1498,12 @@ Update fields on an A_PurchaseRequisitionHeader entity in SAP S/4HANA Cloud (API | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on 204 success, or updated A_PurchaseRequisitionHeader entity if SAP returns one | +| `data` | json | Null on 204 success, or OData v2 envelope with updated A_PurchaseRequisitionHeader at output.data.d | +| ↳ `d` | json | Updated A_PurchaseRequisitionHeader entity \(if returned\) | +| ↳ `PurchaseRequisition` | string | Purchase requisition number | +| ↳ `PurchaseRequisitionType` | string | PR document type | +| ↳ `PurReqnDescription` | string | Purchase requisition description | +| ↳ `SourceDetermination` | string | Source-of-supply determination flag | ### `sap_s4hana_list_purchase_orders` @@ -1001,10 +1513,10 @@ List purchase orders from SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_Pu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -1023,7 +1535,22 @@ List purchase orders from SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_Pu | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_PurchaseOrder entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_PurchaseOrder entities | +| ↳ `PurchaseOrder` | string | Purchase order number | +| ↳ `PurchaseOrderType` | string | PO document type \(e.g., NB\) | +| ↳ `CompanyCode` | string | Company code | +| ↳ `PurchasingOrganization` | string | Purchasing organization | +| ↳ `PurchasingGroup` | string | Purchasing group | +| ↳ `Supplier` | string | Supplier business partner key | +| ↳ `DocumentCurrency` | string | Document currency | +| ↳ `NetAmount` | string | Net amount of the purchase order | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `CreatedByUser` | string | User who created the PO | +| ↳ `PurchaseOrderDate` | string | Purchase order date | +| ↳ `__next` | string | OData skiptoken URL for next page | +| ↳ `__count` | string | Total count when $inlinecount=allpages is used | ### `sap_s4hana_get_purchase_order` @@ -1033,10 +1560,10 @@ Retrieve a single purchase order by PurchaseOrder key from SAP S/4HANA Cloud (AP | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -1052,7 +1579,25 @@ Retrieve a single purchase order by PurchaseOrder key from SAP S/4HANA Cloud (AP | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_PurchaseOrder entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_PurchaseOrder entity | +| ↳ `PurchaseOrder` | string | Purchase order number | +| ↳ `PurchaseOrderType` | string | PO document type | +| ↳ `CompanyCode` | string | Company code | +| ↳ `PurchasingOrganization` | string | Purchasing organization | +| ↳ `PurchasingGroup` | string | Purchasing group | +| ↳ `Supplier` | string | Supplier business partner key | +| ↳ `DocumentCurrency` | string | Document currency | +| ↳ `NetAmount` | string | Net amount of the purchase order | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `CreatedByUser` | string | User who created the PO | +| ↳ `PurchaseOrderDate` | string | Purchase order date | +| ↳ `ValidityStartDate` | string | Validity start date | +| ↳ `ValidityEndDate` | string | Validity end date | +| ↳ `IncotermsClassification` | string | Incoterms classification \(e.g., FOB\) | +| ↳ `PaymentTerms` | string | Payment terms key | +| ↳ `LastChangeDateTime` | string | Last change timestamp \(OData /Date\(ms\)/\) | +| ↳ `to_PurchaseOrderItem` | json | Expanded PO items \(when $expand=to_PurchaseOrderItem\) | ### `sap_s4hana_create_purchase_order` @@ -1062,10 +1607,10 @@ Create a purchase order in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_P | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -1084,20 +1629,31 @@ Create a purchase order in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_P | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Created A_PurchaseOrder entity | +| `data` | json | OData v2 response envelope; created entity at output.data.d | +| ↳ `d` | json | Created A_PurchaseOrder entity | +| ↳ `PurchaseOrder` | string | Auto-assigned purchase order number | +| ↳ `PurchaseOrderType` | string | PO document type | +| ↳ `CompanyCode` | string | Company code | +| ↳ `PurchasingOrganization` | string | Purchasing organization | +| ↳ `PurchasingGroup` | string | Purchasing group | +| ↳ `Supplier` | string | Supplier business partner key | +| ↳ `DocumentCurrency` | string | Document currency | +| ↳ `NetAmount` | string | Net amount of the purchase order | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `to_PurchaseOrderItem` | json | Created PO items returned in deep insert | ### `sap_s4hana_update_purchase_order` -Update fields on an A_PurchaseOrder entity in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. +Update fields on an A_PurchaseOrder header in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. Header-only — line-item changes are not supported via deep update on the header (SAP KBA 2833338); use the A_PurchaseOrderItem entity directly to modify items. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -1113,7 +1669,16 @@ Update fields on an A_PurchaseOrder entity in SAP S/4HANA Cloud (API_PURCHASEORD | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on 204 success, or updated A_PurchaseOrder entity if SAP returns one | +| `data` | json | Null on 204 success, or OData v2 envelope with updated A_PurchaseOrder at output.data.d | +| ↳ `d` | json | Updated A_PurchaseOrder entity \(if returned\) | +| ↳ `PurchaseOrder` | string | Purchase order number | +| ↳ `PurchaseOrderType` | string | PO document type | +| ↳ `CompanyCode` | string | Company code | +| ↳ `PurchasingGroup` | string | Purchasing group | +| ↳ `Supplier` | string | Supplier key | +| ↳ `NetAmount` | string | Net amount | +| ↳ `DocumentCurrency` | string | Document currency | +| ↳ `LastChangeDateTime` | string | Last change timestamp | ### `sap_s4hana_list_supplier_invoices` @@ -1123,10 +1688,10 @@ List supplier invoices from SAP S/4HANA Cloud (API_SUPPLIERINVOICE_PROCESS_SRV, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -1145,7 +1710,26 @@ List supplier invoices from SAP S/4HANA Cloud (API_SUPPLIERINVOICE_PROCESS_SRV, | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_SupplierInvoice entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_SupplierInvoice entities | +| ↳ `SupplierInvoice` | string | Supplier invoice number | +| ↳ `FiscalYear` | string | Fiscal year | +| ↳ `CompanyCode` | string | Company code | +| ↳ `DocumentDate` | string | Invoice document date | +| ↳ `PostingDate` | string | Posting date | +| ↳ `InvoicingParty` | string | Invoicing party \(supplier key\) | +| ↳ `InvoiceGrossAmount` | string | Gross invoice amount | +| ↳ `DocumentCurrency` | string | Document currency | +| ↳ `AccountingDocumentType` | string | Accounting document type | +| ↳ `PaymentTerms` | string | Payment terms key | +| ↳ `DueCalculationBaseDate` | string | Baseline date for due-date calculation | +| ↳ `SupplierInvoiceIDByInvcgParty` | string | Reference number used by the invoicing party | +| ↳ `PaymentMethod` | string | Payment method | +| ↳ `TaxIsCalculatedAutomatically` | boolean | Whether tax is calculated automatically | +| ↳ `ManualCashDiscount` | string | Manually entered cash discount amount | +| ↳ `BusinessPlace` | string | Business place \(jurisdiction code\) | +| ↳ `__next` | string | OData skiptoken URL for next page | ### `sap_s4hana_get_supplier_invoice` @@ -1155,10 +1739,10 @@ Retrieve a single supplier invoice by composite key (SupplierInvoice + FiscalYea | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -1175,20 +1759,37 @@ Retrieve a single supplier invoice by composite key (SupplierInvoice + FiscalYea | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_SupplierInvoice entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_SupplierInvoice entity | +| ↳ `SupplierInvoice` | string | Supplier invoice number | +| ↳ `FiscalYear` | string | Fiscal year | +| ↳ `CompanyCode` | string | Company code | +| ↳ `DocumentDate` | string | Invoice document date | +| ↳ `PostingDate` | string | Posting date | +| ↳ `InvoicingParty` | string | Invoicing party \(supplier key\) | +| ↳ `InvoiceGrossAmount` | string | Gross invoice amount | +| ↳ `DocumentCurrency` | string | Document currency | +| ↳ `AccountingDocumentType` | string | Accounting document type | +| ↳ `PaymentTerms` | string | Payment terms key | +| ↳ `DueCalculationBaseDate` | string | Baseline date for due-date calculation | +| ↳ `SupplierInvoiceIDByInvcgParty` | string | Reference number used by the invoicing party | +| ↳ `PaymentMethod` | string | Payment method | +| ↳ `TaxIsCalculatedAutomatically` | boolean | Whether tax is calculated automatically | +| ↳ `ManualCashDiscount` | string | Manually entered cash discount amount | +| ↳ `BusinessPlace` | string | Business place \(jurisdiction code\) | ### `sap_s4hana_odata_query` -Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping. +Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping. For write operations (POST/PUT/PATCH/MERGE/DELETE), pass an If-Match ETag obtained from a prior GET to avoid lost updates; misuse will mutate production data. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index d1dc93e0a96..e19d3d267b1 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -155,6 +155,7 @@ import { RootlyIcon, S3Icon, SalesforceIcon, + SapConcurIcon, SapS4HanaIcon, SESIcon, SearchIcon, @@ -354,6 +355,7 @@ export const blockTypeToIconMap: Record = { rootly: RootlyIcon, s3: S3Icon, salesforce: SalesforceIcon, + sap_concur: SapConcurIcon, sap_s4hana: SapS4HanaIcon, search: SearchIcon, secrets_manager: SecretsManagerIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 113c411c190..69245b5cb8b 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -11482,6 +11482,305 @@ "integrationTypes": ["crm", "customer-support", "sales"], "tags": ["sales-engagement", "customer-support"] }, + { + "type": "sap_concur", + "slug": "sap-concur", + "name": "SAP Concur", + "description": "Manage expense reports, travel requests, cash advances, and more in SAP Concur", + "longDescription": "Connect SAP Concur via OAuth 2.0. Manage expense reports and line items, allocations, attendees, comments, exceptions, quick expenses, receipts, travel requests and expected expenses, cash advances, itineraries, user identities, custom lists, budgets, exchange rates, and purchase requests across every Concur datacenter.", + "bgColor": "#FFFFFF", + "iconName": "SapConcurIcon", + "docsUrl": "https://docs.sim.ai/tools/sap_concur", + "operations": [ + { + "name": "List Expense Reports", + "description": "List expense reports (GET /api/v3.0/expense/reports). Returns a v3 envelope { Items, NextPage }." + }, + { + "name": "Get Expense Report", + "description": "Retrieve a single expense report header by id via Expense Report v4 (/expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId})." + }, + { + "name": "Create Expense Report", + "description": "Create an expense report (POST /expensereports/v4/users/{userId}/context/{contextType}/reports — supported contexts: TRAVELER, PROXY). Required body fields: name, policyId." + }, + { + "name": "Update Expense Report", + "description": "Update an unsubmitted expense report (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId} — supported contexts: TRAVELER, PROXY). Body fields: businessPurpose, comment, customData, name, etc." + }, + { + "name": "Delete Expense Report", + "description": "Delete an expense report (DELETE /expensereports/v4/reports/{reportId})." + }, + { + "name": "Submit Expense Report", + "description": "Submit an expense report into the workflow via Expense Report v4 (PATCH /expensereports/v4/users/{userId}/reports/{reportId}/submit)." + }, + { + "name": "Recall Expense Report", + "description": "Recall a submitted expense report (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/recall — supported contexts: TRAVELER, PROXY). No request body is required." + }, + { + "name": "Approve Expense Report", + "description": "Approve an expense report as a manager (PATCH /expensereports/v4/users/{userId}/context/MANAGER/reports/{reportId}/approve)." + }, + { + "name": "Send Back Expense Report", + "description": "Send back an expense report to the employee (PATCH /expensereports/v4/users/{userId}/context/MANAGER/reports/{reportId}/sendBack)." + }, + { + "name": "List Reports To Approve", + "description": "List expense reports awaiting approval (GET /expensereports/v4/users/{userId}/context/MANAGER/reportsToApprove)." + }, + { + "name": "List Expenses", + "description": "List expenses on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses)." + }, + { + "name": "Get Expense", + "description": "Get a single expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId})." + }, + { + "name": "Update Expense", + "description": "Update an expense (PATCH /expensereports/v4/reports/{reportId}/expenses/{expenseId})." + }, + { + "name": "Delete Expense", + "description": "Delete an expense (DELETE /expensereports/v4/reports/{reportId}/expenses/{expenseId})." + }, + { + "name": "Get Itemizations", + "description": "Get expense itemizations (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/itemizations)." + }, + { + "name": "List Allocations", + "description": "List allocations on an expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/allocations)." + }, + { + "name": "Get Allocation", + "description": "Get a single allocation (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/allocations/{allocationId})." + }, + { + "name": "Update Allocation", + "description": "Update an allocation (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/allocations/{allocationId})." + }, + { + "name": "List Attendee Associations", + "description": "List attendees associated with an expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/attendees)." + }, + { + "name": "Associate Attendees", + "description": "Associate attendees with an expense (POST /expensereports/v4/users/{userId}/context/TRAVELER/reports/{reportId}/expenses/{expenseId}/attendees)." + }, + { + "name": "Remove All Attendees", + "description": "Remove all attendees from an expense (DELETE /expensereports/v4/users/{userId}/context/TRAVELER/reports/{reportId}/expenses/{expenseId}/attendees)." + }, + { + "name": "List Report Comments", + "description": "List comments on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/comments)." + }, + { + "name": "Create Report Comment", + "description": "Create a comment on a report (POST /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/comments)." + }, + { + "name": "List Exceptions", + "description": "List exceptions on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/exceptions)." + }, + { + "name": "Create Quick Expense", + "description": "Create a quick expense (POST /quickexpense/v4/users/{userId}/context/TRAVELER/quickexpenses)." + }, + { + "name": "Create Quick Expense (With Image)", + "description": "Create a quick expense with an attached image (POST /quickexpense/v4/users/{userId}/context/{contextType}/quickexpenses/image)." + }, + { + "name": "List Receipts", + "description": "List receipts for a user (GET /receipts/v4/users/{userId})." + }, + { + "name": "Get Receipt", + "description": "Get a single receipt by ID (GET /receipts/v4/{receiptId})." + }, + { + "name": "Get Receipt Status", + "description": "Get receipt processing status (GET /receipts/v4/status/{receiptId})." + }, + { + "name": "Upload Receipt Image", + "description": "Upload an image-only receipt (POST /receipts/v4/users/{userId}/image-only-receipts)." + }, + { + "name": "List Travel Requests", + "description": "List travel requests (GET /travelrequest/v4/requests)." + }, + { + "name": "Get Travel Request", + "description": "Get a single travel request (GET /travelrequest/v4/requests/{requestUuid})." + }, + { + "name": "Create Travel Request", + "description": "Create a travel request (POST /travelrequest/v4/requests)." + }, + { + "name": "Update Travel Request", + "description": "Update a travel request (PUT /travelrequest/v4/requests/{requestUuid})." + }, + { + "name": "Delete Travel Request", + "description": "Delete a travel request (DELETE /travelrequest/v4/requests/{requestUuid})." + }, + { + "name": "Move Travel Request (Workflow Action)", + "description": "Move a travel request through workflow (POST /travelrequest/v4/requests/{requestUuid}/{action}). Valid actions: submit, recall, cancel, approve, sendback, close, reopen." + }, + { + "name": "List Travel Request Comments", + "description": "List comments on a travel request (GET /travelrequest/v4/requests/{requestUuid}/comments)." + }, + { + "name": "Get Request Cash Advance", + "description": "Get a single cash advance assigned to a travel request (GET /travelrequest/v4/cashadvances/{cashAdvanceUuid})." + }, + { + "name": "Create Expected Expense", + "description": "Create an expected expense on a travel request (POST /travelrequest/v4/requests/{requestUuid}/expenses)." + }, + { + "name": "List Expected Expenses", + "description": "List expected expenses on a travel request (GET /travelrequest/v4/requests/{requestUuid}/expenses)." + }, + { + "name": "Get Expected Expense", + "description": "Get an expected expense (GET /travelrequest/v4/expenses/{expenseUuid})." + }, + { + "name": "Update Expected Expense", + "description": "Update an expected expense (PUT /travelrequest/v4/expenses/{expenseUuid})." + }, + { + "name": "Delete Expected Expense", + "description": "Delete an expected expense (DELETE /travelrequest/v4/expenses/{expenseUuid})." + }, + { + "name": "Create Cash Advance", + "description": "Create a cash advance (POST /cashadvance/v4/cashadvances)." + }, + { + "name": "Get Cash Advance", + "description": "Get a cash advance (GET /cashadvance/v4/cashadvances/{cashadvanceId})." + }, + { + "name": "Issue Cash Advance", + "description": "Issue a cash advance (POST /cashadvance/v4/cashadvances/{cashadvanceId}/issue)." + }, + { + "name": "List Itineraries (Trips)", + "description": "List travel trips/itineraries (GET /api/travel/trip/v1.1)." + }, + { + "name": "Get Itinerary (Trip)", + "description": "Get a single trip/itinerary (GET /api/travel/trip/v1.1/{tripID})." + }, + { + "name": "List Users", + "description": "List Concur user identities (GET /profile/identity/v4.1/Users)." + }, + { + "name": "Get User", + "description": "Get a single user by UUID (GET /profile/identity/v4.1/Users/{id})." + }, + { + "name": "Create User", + "description": "Create a new user identity (POST /profile/identity/v4.1/Users)." + }, + { + "name": "Update User (PATCH)", + "description": "Patch a user identity (PATCH /profile/identity/v4.1/Users/{id})." + }, + { + "name": "Delete User", + "description": "Delete a user identity (DELETE /profile/identity/v4.1/Users/{id})." + }, + { + "name": "Search Users", + "description": "Search users via SCIM .search endpoint (POST /profile/identity/v4.1/Users/.search)." + }, + { + "name": "List Lists", + "description": "List custom lists (GET /list/v4/lists)." + }, + { + "name": "Get List", + "description": "Get a single custom list (GET /list/v4/lists/{listId})." + }, + { + "name": "List List Items", + "description": "List the top-level items (children) for a custom list (GET /list/v4/lists/{listId}/children)." + }, + { + "name": "Get List Item", + "description": "Get a single list item (GET /list/v4/items/{itemId})." + }, + { + "name": "Create List Item", + "description": "Create a list item (POST /list/v4/items)." + }, + { + "name": "Update List Item", + "description": "Update a list item (PUT /list/v4/items/{itemId})." + }, + { + "name": "Delete List Item", + "description": "Delete a list item (DELETE /list/v4/items/{itemId})." + }, + { + "name": "List Budgets", + "description": "List budget item headers (GET /budget/v4/budgetItemHeader)." + }, + { + "name": "Get Budget", + "description": "Get a budget item header by ID (GET /budget/v4/budgetItemHeader/{id})." + }, + { + "name": "List Budget Categories", + "description": "List budget categories (GET /budget/v4/budgetCategory)." + }, + { + "name": "Get Exchange Rate", + "description": "Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body: { currency_sets: [{ from_crn_code, to_crn_code, start_date: " + }, + { + "name": "Create Purchase Request", + "description": "Create a purchase request (POST /purchaserequest/v4/purchaserequests)." + }, + { + "name": "Get Purchase Request", + "description": "Get a purchase request by ID (GET /purchaserequest/v4/purchaserequests/{id})." + }, + { + "name": "Get Travel Profile", + "description": "Get a travel profile (GET /api/travelprofile/v2.0/profile). Returns the calling user by default; pass userid_type and userid_value to impersonate." + }, + { + "name": "List Travel Profiles Summary", + "description": "List travel profile summaries (GET /api/travelprofile/v2.0/summary). LastModifiedDate is required by Concur." + }, + { + "name": "Search Locations", + "description": "Search Concur location reference data (GET /localities/v5/locations)." + } + ], + "operationCount": 70, + "triggers": [], + "triggerCount": 0, + "authType": "none", + "category": "tools", + "integrationTypes": ["other", "developer-tools"], + "tags": ["automation"] + }, { "type": "sap_s4hana", "slug": "sap-s4hana", @@ -11506,7 +11805,7 @@ }, { "name": "Update Business Partner", - "description": "Update fields on an A_BusinessPartner entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates." + "description": "Update fields on an A_BusinessPartner entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. Deep updates on nested associations (e.g. to_BusinessPartnerAddress) are not supported by SAP (KBA 2833338) — use the dedicated child endpoints." }, { "name": "List Customers", @@ -11518,7 +11817,7 @@ }, { "name": "Update Customer", - "description": "Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. A_Customer PATCH is limited to modifiable fields such as OrderIsBlockedForCustomer, DeliveryIsBlock, BillingIsBlockedForCustomer, PostingIsBlocked, and DeletionIndicator. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates." + "description": "Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. A_Customer is limited to modifiable fields such as OrderIsBlockedForCustomer, DeliveryIsBlocked, BillingIsBlockedForCustomer (Edm.String reason codes like " }, { "name": "List Suppliers", @@ -11530,7 +11829,7 @@ }, { "name": "Update Supplier", - "description": "Update fields on an A_Supplier entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. A_Supplier PATCH is limited to modifiable fields such as PostingIsBlocked, PurchasingIsBlocked, PaymentIsBlockedForSupplier, DeletionIndicator, and SupplierAccountGroup. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates." + "description": "" }, { "name": "List Sales Orders", @@ -11546,7 +11845,7 @@ }, { "name": "Update Sales Order", - "description": "Update fields on an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates." + "description": "" }, { "name": "Delete Sales Order", @@ -11586,7 +11885,7 @@ }, { "name": "Update Product", - "description": "Update fields on an A_Product entity in SAP S/4HANA Cloud (API_PRODUCT_SRV). PATCH only sends the fields you provide; existing values are preserved. Flat scalar header fields only — deep/multi-entity updates across navigation properties are not supported by API_PRODUCT_SRV PATCH/PUT (see SAP KBA 2833338); update child entities (plant, valuation, sales data, etc.) via their own endpoints. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET." + "description": "" }, { "name": "List Material Stock", @@ -11614,7 +11913,7 @@ }, { "name": "Update Purchase Requisition", - "description": "Update fields on an A_PurchaseRequisitionHeader entity in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV; deprecated since S/4HANA 2402, successor is API_PURCHASEREQUISITION_2 OData v4). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates." + "description": "" }, { "name": "List Purchase Orders", @@ -11630,7 +11929,7 @@ }, { "name": "Update Purchase Order", - "description": "Update fields on an A_PurchaseOrder entity in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates." + "description": "Update fields on an A_PurchaseOrder header in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. Header-only — line-item changes are not supported via deep update on the header (SAP KBA 2833338); use the A_PurchaseOrderItem entity directly to modify items. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates." }, { "name": "List Supplier Invoices", @@ -11642,7 +11941,7 @@ }, { "name": "OData Query (advanced)", - "description": "Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping." + "description": "Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping. For write operations (POST/PUT/PATCH/MERGE/DELETE), pass an If-Match ETag obtained from a prior GET to avoid lost updates; misuse will mutate production data." } ], "operationCount": 38, diff --git a/apps/sim/app/api/tools/sap_concur/proxy/route.ts b/apps/sim/app/api/tools/sap_concur/proxy/route.ts new file mode 100644 index 00000000000..802d83be266 --- /dev/null +++ b/apps/sim/app/api/tools/sap_concur/proxy/route.ts @@ -0,0 +1,133 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { getValidationErrorMessage, isZodError } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { + assertSafeExternalUrl, + extractSapConcurError, + fetchSapConcurAccessToken, + SAP_CONCUR_OUTBOUND_FETCH_TIMEOUT_MS, + type SapConcurProxyRequest, + SapConcurProxyRequestSchema, +} from '@/app/api/tools/sap_concur/shared' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SapConcurProxyAPI') + +type ProxyRequest = SapConcurProxyRequest + +function buildApiUrl(geolocation: string, req: ProxyRequest): string { + const base = geolocation.replace(/\/+$/, '') + const subPath = req.path.startsWith('/') ? req.path : `/${req.path}` + const url = `${base}${subPath}` + + if (!req.query || Object.keys(req.query).length === 0) { + return url + } + const search = new URLSearchParams() + for (const [key, value] of Object.entries(req.query)) { + if (value === undefined || value === null) continue + search.append(key, String(value)) + } + const queryString = search.toString() + if (!queryString) return url + return url.includes('?') ? `${url}&${queryString}` : `${url}?${queryString}` +} + +interface Invocation { + status: number + body: unknown + raw: string +} + +async function callConcur( + req: ProxyRequest, + accessToken: string, + geolocation: string +): Promise { + const url = assertSafeExternalUrl(buildApiUrl(geolocation, req), 'apiUrl').toString() + const hasBody = req.body !== undefined && req.body !== null + const headers: Record = { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + } + if (hasBody) headers['Content-Type'] = req.contentType ?? 'application/json' + if (req.companyUuid) headers['concur-correlationid'] = req.companyUuid + + const response = await secureFetchWithValidation( + url, + { + method: req.method, + headers, + body: hasBody + ? typeof req.body === 'string' + ? req.body + : JSON.stringify(req.body) + : undefined, + timeout: SAP_CONCUR_OUTBOUND_FETCH_TIMEOUT_MS, + }, + 'apiUrl' + ) + + const raw = await response.text() + let parsed: unknown = null + if (raw.length > 0) { + try { + parsed = JSON.parse(raw) + } catch { + parsed = raw + } + } + return { status: response.status, body: parsed, raw } +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Concur proxy request: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + // boundary-raw-json: internal proxy envelope validated by SapConcurProxyRequestSchema below; not a public boundary + const json = await request.json() + const proxyReq = SapConcurProxyRequestSchema.parse(json) + + const { accessToken, geolocation } = await fetchSapConcurAccessToken(proxyReq, requestId) + const invocation = await callConcur(proxyReq, accessToken, geolocation) + + if (invocation.status >= 200 && invocation.status < 300) { + const data = invocation.status === 204 ? null : invocation.body + return NextResponse.json({ success: true, output: { status: invocation.status, data } }) + } + + const message = extractSapConcurError(invocation.body, invocation.status) + logger.warn( + `[${requestId}] Concur API error (${invocation.status}) ${proxyReq.path}: ${message}` + ) + return NextResponse.json( + { success: false, error: message, status: invocation.status }, + { status: invocation.status } + ) + } catch (error) { + if (isZodError(error)) { + logger.warn(`[${requestId}] Validation error:`, error.issues) + return NextResponse.json( + { success: false, error: getValidationErrorMessage(error, 'Validation failed') }, + { status: 400 } + ) + } + logger.error(`[${requestId}] Unexpected Concur proxy error:`, error) + return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/sap_concur/shared.ts b/apps/sim/app/api/tools/sap_concur/shared.ts new file mode 100644 index 00000000000..dd5fd5aa1b7 --- /dev/null +++ b/apps/sim/app/api/tools/sap_concur/shared.ts @@ -0,0 +1,300 @@ +import { createHash } from 'node:crypto' +import { createLogger } from '@sim/logger' +import { z } from 'zod' +import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' +import { FileInputSchema } from '@/lib/uploads/utils/file-schemas' + +const logger = createLogger('SapConcurShared') + +export const SAP_CONCUR_ALLOWED_DATACENTERS = new Set([ + 'us.api.concursolutions.com', + 'us2.api.concursolutions.com', + 'eu.api.concursolutions.com', + 'eu2.api.concursolutions.com', + 'cn.api.concursolutions.com', + 'emea.api.concursolutions.com', +]) + +export const SapConcurDatacenterSchema = z + .string() + .min(1) + .refine((d) => SAP_CONCUR_ALLOWED_DATACENTERS.has(d), { + message: `datacenter must be one of: ${Array.from(SAP_CONCUR_ALLOWED_DATACENTERS).join(', ')}`, + }) + +export const SapConcurGrantTypeSchema = z.enum(['client_credentials', 'password', 'refresh_token']) + +export const SapConcurAuthSchema = z.object({ + datacenter: SapConcurDatacenterSchema.default('us.api.concursolutions.com'), + grantType: SapConcurGrantTypeSchema.default('client_credentials'), + clientId: z.string().min(1, 'clientId is required'), + clientSecret: z.string().min(1, 'clientSecret is required'), + username: z.string().optional(), + password: z.string().optional(), + companyUuid: z.string().optional(), +}) + +export type SapConcurAuth = z.infer + +export const SapConcurHttpMethod = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) + +export const SapConcurProxyPath = z + .string() + .min(1, 'path is required') + .refine( + (p) => + !p.split(/[/\\]/).some((seg) => seg === '..' || seg === '.') && + !p.includes('#') && + !/%(?:2[eEfF]|5[cC]|23)/.test(p), + { + message: + 'path must not contain ".." or "." segments, "#", or percent-encoded path/fragment characters', + } + ) + +export const SapConcurProxyRequestSchema = SapConcurAuthSchema.extend({ + path: SapConcurProxyPath, + method: SapConcurHttpMethod.default('GET'), + query: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(), + body: z.unknown().optional(), + contentType: z.string().optional(), +}).superRefine((req, ctx) => { + if (req.grantType === 'password') { + if (!req.username) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['username'], + message: 'username is required for password grant', + }) + } + if (!req.password) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['password'], + message: 'password is required for password grant', + }) + } + } +}) + +export type SapConcurProxyRequest = z.infer + +export const SapConcurUploadOperation = z.enum([ + 'upload_receipt_image', + 'create_quick_expense_with_image', +]) + +export const SapConcurUploadRequestSchema = SapConcurAuthSchema.extend({ + operation: SapConcurUploadOperation, + userId: z.string().min(1, 'userId is required'), + contextType: z.string().optional(), + receipt: FileInputSchema, + forwardId: z.string().max(40).optional(), + body: z.union([z.record(z.string(), z.unknown()), z.string()]).optional(), +}) + +export type SapConcurUploadRequest = z.infer + +const FORBIDDEN_HOSTS = new Set([ + 'localhost', + '0.0.0.0', + '127.0.0.1', + '169.254.169.254', + 'metadata.google.internal', + 'metadata', + '[::1]', + '[::]', + '[::ffff:127.0.0.1]', + '[fd00:ec2::254]', +]) + +function isPrivateIPv4(host: string): boolean { + const match = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) + if (!match) return false + const octets = match.slice(1, 5).map(Number) as [number, number, number, number] + if (octets.some((o) => o < 0 || o > 255)) return false + const [a, b] = octets + if (a === 10) return true + if (a === 172 && b >= 16 && b <= 31) return true + if (a === 192 && b === 168) return true + if (a === 127) return true + if (a === 169 && b === 254) return true + if (a === 0) return true + return false +} + +function isPrivateOrLoopbackIPv6(host: string): boolean { + const stripped = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host + const lower = stripped.toLowerCase() + if (lower === '::' || lower === '::1') return true + if (/^fc[0-9a-f]{2}:/.test(lower) || /^fd[0-9a-f]{2}:/.test(lower)) return true + if (lower.startsWith('fe80:')) return true + return false +} + +/** Validate a URL is https and not pointing to a private/loopback host. */ +export function assertSafeExternalUrl(rawUrl: string, label: string): URL { + let parsed: URL + try { + parsed = new URL(rawUrl) + } catch { + throw new Error(`${label} must be a valid URL`) + } + if (parsed.protocol !== 'https:') { + throw new Error(`${label} must use https://`) + } + const host = parsed.hostname.toLowerCase() + if (FORBIDDEN_HOSTS.has(host) || FORBIDDEN_HOSTS.has(`[${host}]`)) { + throw new Error(`${label} host is not allowed`) + } + if (isPrivateIPv4(host)) { + throw new Error(`${label} host is not allowed (private/loopback range)`) + } + if (isPrivateOrLoopbackIPv6(host)) { + throw new Error(`${label} host is not allowed (IPv6 private/loopback)`) + } + return parsed +} + +interface CachedToken { + accessToken: string + geolocation: string + expiresAt: number +} + +const TOKEN_CACHE = new Map() +const TOKEN_CACHE_MAX_ENTRIES = 500 +const TOKEN_SAFETY_WINDOW_MS = 60_000 +export const SAP_CONCUR_OUTBOUND_FETCH_TIMEOUT_MS = 30_000 + +function tokenCacheKey(req: SapConcurAuth): string { + const secretHash = createHash('sha256').update(req.clientSecret).digest('hex').slice(0, 16) + const userHash = req.username + ? createHash('sha256').update(req.username).digest('hex').slice(0, 12) + : '' + return `${req.datacenter}::${req.grantType}::${req.clientId}::${secretHash}::${userHash}` +} + +function rememberToken(key: string, token: CachedToken): void { + if (TOKEN_CACHE.has(key)) TOKEN_CACHE.delete(key) + TOKEN_CACHE.set(key, token) + while (TOKEN_CACHE.size > TOKEN_CACHE_MAX_ENTRIES) { + const oldestKey = TOKEN_CACHE.keys().next().value + if (oldestKey === undefined) break + TOKEN_CACHE.delete(oldestKey) + } +} + +function normalizeGeolocation(raw: string | undefined, fallback: string): string { + if (!raw) return `https://${fallback}` + const trimmed = raw.replace(/\/+$/, '') + if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) return trimmed + return `https://${trimmed}` +} + +/** + * Acquire a Concur access token, sharing a cache with the proxy route. + * Validates that the geolocation returned by Concur is a safe external URL. + */ +export async function fetchSapConcurAccessToken( + auth: SapConcurAuth, + requestId: string +): Promise<{ accessToken: string; geolocation: string }> { + if (auth.grantType === 'password') { + if (!auth.username) throw new Error('username is required for password grant') + if (!auth.password) throw new Error('password is required for password grant') + } + + const cacheKey = tokenCacheKey(auth) + const cached = TOKEN_CACHE.get(cacheKey) + if (cached && cached.expiresAt - TOKEN_SAFETY_WINDOW_MS > Date.now()) { + return { accessToken: cached.accessToken, geolocation: cached.geolocation } + } + + const tokenUrl = assertSafeExternalUrl( + `https://${auth.datacenter}/oauth2/v0/token`, + 'tokenUrl' + ).toString() + + const params = new URLSearchParams() + params.set('client_id', auth.clientId) + params.set('client_secret', auth.clientSecret) + params.set('grant_type', auth.grantType) + if (auth.grantType === 'password') { + params.set('username', auth.username ?? '') + params.set('password', auth.password ?? '') + if (auth.companyUuid) params.set('credtype', 'authtoken') + } + + const response = await secureFetchWithValidation( + tokenUrl, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }, + body: params.toString(), + timeout: SAP_CONCUR_OUTBOUND_FETCH_TIMEOUT_MS, + }, + 'tokenUrl' + ) + + if (!response.ok) { + const text = await response.text().catch(() => '') + logger.warn(`[${requestId}] Concur token fetch failed (${response.status}): ${text}`) + throw new Error(`Concur token request failed: HTTP ${response.status}`) + } + + const data = (await response.json()) as { + access_token?: string + expires_in?: number + geolocation?: string + } + + if (!data.access_token) { + throw new Error('Concur token response missing access_token') + } + + const geolocation = normalizeGeolocation(data.geolocation, auth.datacenter) + assertSafeExternalUrl(geolocation, 'geolocation') + + const expiresInMs = (data.expires_in ?? 3600) * 1000 + rememberToken(cacheKey, { + accessToken: data.access_token, + geolocation, + expiresAt: Date.now() + expiresInMs, + }) + return { accessToken: data.access_token, geolocation } +} + +/** Extract a meaningful error message from a Concur error response body. */ +export function extractSapConcurError(body: unknown, status: number): string { + if (body && typeof body === 'object') { + const obj = body as Record + if (typeof obj.error === 'string' && obj.error.length > 0) { + const desc = typeof obj.error_description === 'string' ? `: ${obj.error_description}` : '' + return `${obj.error}${desc}` + } + if (typeof obj.message === 'string' && obj.message.length > 0) { + return obj.message + } + const errors = obj.errors + if (Array.isArray(errors) && errors.length > 0) { + return errors + .map((e) => { + if (e && typeof e === 'object') { + const eo = e as Record + const code = typeof eo.errorCode === 'string' ? `[${eo.errorCode}] ` : '' + const msg = typeof eo.errorMessage === 'string' ? eo.errorMessage : '' + return `${code}${msg}`.trim() + } + return String(e) + }) + .filter(Boolean) + .join('; ') + } + } + if (typeof body === 'string' && body.length > 0) return body + return `Concur request failed with HTTP ${status}` +} diff --git a/apps/sim/app/api/tools/sap_concur/upload/route.ts b/apps/sim/app/api/tools/sap_concur/upload/route.ts new file mode 100644 index 00000000000..81885d9a29f --- /dev/null +++ b/apps/sim/app/api/tools/sap_concur/upload/route.ts @@ -0,0 +1,279 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { getValidationErrorMessage, isZodError } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { secureFetchWithValidation } 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' +import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { + assertSafeExternalUrl, + extractSapConcurError, + fetchSapConcurAccessToken, + SAP_CONCUR_OUTBOUND_FETCH_TIMEOUT_MS, + type SapConcurUploadRequest, + SapConcurUploadRequestSchema, +} from '@/app/api/tools/sap_concur/shared' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SapConcurUploadAPI') + +type UploadRequest = SapConcurUploadRequest + +const RECEIPT_ALLOWED_MIME_TYPES = new Set([ + 'application/pdf', + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/gif', + 'image/tiff', +]) + +const QUICK_EXPENSE_ALLOWED_MIME_TYPES = new Set([ + 'application/pdf', + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/tiff', +]) + +const ALLOWED_MIME_TYPES = RECEIPT_ALLOWED_MIME_TYPES + +function inferMimeType(name: string, declared?: string): string { + if (declared && ALLOWED_MIME_TYPES.has(declared.toLowerCase())) { + return declared.toLowerCase() === 'image/jpg' ? 'image/jpeg' : declared.toLowerCase() + } + const lower = name.toLowerCase() + if (lower.endsWith('.pdf')) return 'application/pdf' + if (lower.endsWith('.png')) return 'image/png' + if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg' + if (lower.endsWith('.gif')) return 'image/gif' + if (lower.endsWith('.tif') || lower.endsWith('.tiff')) return 'image/tiff' + return 'application/octet-stream' +} + +function stringifyMaybeJson(value: unknown): string { + if (typeof value === 'string') return value + return JSON.stringify(value ?? {}) +} + +interface UploadInvocation { + status: number + body: unknown +} + +async function postMultipart( + url: string, + accessToken: string, + formData: FormData, + companyUuid: string | undefined, + extraHeaders?: Record +): Promise { + const headers: Record = { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + ...(extraHeaders ?? {}), + } + if (companyUuid) headers['concur-correlationid'] = companyUuid + + // Serialize FormData (with auto-generated multipart boundary) to a Buffer so we can + // route through secureFetchWithValidation (which doesn't support FormData bodies directly). + const serialized = new Request('http://localhost/internal-multipart-serializer', { + method: 'POST', + body: formData, + }) + const contentType = serialized.headers.get('content-type') + if (contentType) headers['Content-Type'] = contentType + const bodyBuffer = Buffer.from(await serialized.arrayBuffer()) + + const response = await secureFetchWithValidation( + url, + { + method: 'POST', + headers, + body: bodyBuffer, + timeout: SAP_CONCUR_OUTBOUND_FETCH_TIMEOUT_MS, + }, + 'apiUrl' + ) + + const raw = await response.text() + let parsed: unknown = null + if (raw.length > 0) { + try { + parsed = JSON.parse(raw) + } catch { + parsed = raw + } + } + // Surface Location/Link headers for receipt endpoints that return 202 with no body. + if ( + parsed === null || + (typeof parsed === 'object' && parsed !== null && Object.keys(parsed).length === 0) + ) { + const location = response.headers.get('Location') + const link = response.headers.get('Link') + if (location || link) { + parsed = { location, link } + } + } + return { status: response.status, body: parsed } +} + +async function handleUploadReceiptImage( + req: UploadRequest, + fileBuffer: Buffer, + fileName: string, + mimeType: string, + accessToken: string, + geolocation: string +): Promise { + const url = assertSafeExternalUrl( + `${geolocation.replace(/\/+$/, '')}/receipts/v4/users/${encodeURIComponent(req.userId)}/image-only-receipts`, + 'apiUrl' + ).toString() + + const formData = new FormData() + formData.append('image', new Blob([new Uint8Array(fileBuffer)], { type: mimeType }), fileName) + + const extraHeaders: Record | undefined = req.forwardId + ? { 'concur-forwardid': req.forwardId } + : undefined + + return postMultipart(url, accessToken, formData, req.companyUuid, extraHeaders) +} + +async function handleCreateQuickExpenseWithImage( + req: UploadRequest, + fileBuffer: Buffer, + fileName: string, + mimeType: string, + accessToken: string, + geolocation: string +): Promise { + const contextType = req.contextType?.trim() || 'TRAVELER' + const url = assertSafeExternalUrl( + `${geolocation.replace(/\/+$/, '')}/quickexpense/v4/users/${encodeURIComponent( + req.userId + )}/context/${encodeURIComponent(contextType)}/quickexpenses/image`, + 'apiUrl' + ).toString() + + const quickExpenseRequest = stringifyMaybeJson(req.body ?? {}) + + const formData = new FormData() + formData.append('quickExpenseRequest', quickExpenseRequest) + formData.append( + 'fileContent', + new Blob([new Uint8Array(fileBuffer)], { type: mimeType }), + fileName + ) + + return postMultipart(url, accessToken, formData, req.companyUuid) +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Concur upload request: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + // boundary-raw-json: internal upload envelope validated by SapConcurUploadRequestSchema below; not a public boundary + const json = await request.json() + const uploadReq = SapConcurUploadRequestSchema.parse(json) + + const userFiles = processFilesToUserFiles( + [uploadReq.receipt as RawFileInput], + requestId, + logger + ) + if (userFiles.length === 0) { + return NextResponse.json( + { success: false, error: 'Invalid receipt file input' }, + { status: 400 } + ) + } + const userFile = userFiles[0] + const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) + const fileName = userFile.name + const mimeType = inferMimeType(fileName, userFile.type) + + const allowedForOperation = + uploadReq.operation === 'create_quick_expense_with_image' + ? QUICK_EXPENSE_ALLOWED_MIME_TYPES + : RECEIPT_ALLOWED_MIME_TYPES + if (!allowedForOperation.has(mimeType)) { + const allowedLabel = + uploadReq.operation === 'create_quick_expense_with_image' + ? 'pdf, png, jpeg, tiff' + : 'pdf, png, jpeg, gif, tiff' + return NextResponse.json( + { + success: false, + error: `Unsupported receipt mime type: ${mimeType}. Allowed: ${allowedLabel}`, + }, + { status: 400 } + ) + } + + const { accessToken, geolocation } = await fetchSapConcurAccessToken(uploadReq, requestId) + + let invocation: UploadInvocation + if (uploadReq.operation === 'upload_receipt_image') { + invocation = await handleUploadReceiptImage( + uploadReq, + fileBuffer, + fileName, + mimeType, + accessToken, + geolocation + ) + } else { + invocation = await handleCreateQuickExpenseWithImage( + uploadReq, + fileBuffer, + fileName, + mimeType, + accessToken, + geolocation + ) + } + + if (invocation.status >= 200 && invocation.status < 300) { + const data = invocation.status === 204 ? null : invocation.body + logger.info( + `[${requestId}] Concur ${uploadReq.operation} succeeded: HTTP ${invocation.status}` + ) + return NextResponse.json({ success: true, output: { status: invocation.status, data } }) + } + + const message = extractSapConcurError(invocation.body, invocation.status) + logger.warn( + `[${requestId}] Concur upload error (${invocation.status}) ${uploadReq.operation}: ${message}` + ) + return NextResponse.json( + { success: false, error: message, status: invocation.status }, + { status: invocation.status } + ) + } catch (error) { + if (isZodError(error)) { + logger.warn(`[${requestId}] Validation error:`, error.issues) + return NextResponse.json( + { success: false, error: getValidationErrorMessage(error, 'Validation failed') }, + { status: 400 } + ) + } + logger.error(`[${requestId}] Unexpected Concur upload error:`, error) + return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/sap_s4hana/proxy/route.ts b/apps/sim/app/api/tools/sap_s4hana/proxy/route.ts index d3414a48af1..ae1e9c06054 100644 --- a/apps/sim/app/api/tools/sap_s4hana/proxy/route.ts +++ b/apps/sim/app/api/tools/sap_s4hana/proxy/route.ts @@ -9,6 +9,10 @@ import { } from '@/lib/api/contracts/tools/sap' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + type SecureFetchResponse, + secureFetchWithValidation, +} from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -65,16 +69,20 @@ async function fetchAccessToken(req: ProxyRequest, requestId: string): Promise '') @@ -104,11 +112,8 @@ interface CsrfBundle { cookie: string } -function joinSetCookies(headers: Headers): string { - const cookies = - typeof (headers as { getSetCookie?: () => string[] }).getSetCookie === 'function' - ? (headers as { getSetCookie: () => string[] }).getSetCookie() - : (headers.get('set-cookie') ?? '').split(/,\s*(?=[^=,;\s]+=)/) +function joinSetCookies(response: SecureFetchResponse): string { + const cookies = (response.headers.get('set-cookie') ?? '').split(/,\s*(?=[^=,;\s]+=)/) return cookies .map((c) => c.split(';')[0]?.trim()) .filter(Boolean) @@ -129,15 +134,19 @@ async function fetchCsrf( requestId: string ): Promise { const url = buildOdataUrl(req, '/$metadata') - const response = await fetch(url, { - method: 'GET', - headers: { - Authorization: buildAuthHeader(req, accessToken), - Accept: 'application/xml', - 'X-CSRF-Token': 'Fetch', + const response = await secureFetchWithValidation( + url, + { + method: 'GET', + headers: { + Authorization: buildAuthHeader(req, accessToken), + Accept: 'application/xml', + 'X-CSRF-Token': 'Fetch', + }, + timeout: OUTBOUND_FETCH_TIMEOUT_MS, }, - signal: AbortSignal.timeout(OUTBOUND_FETCH_TIMEOUT_MS), - }) + 'baseUrl' + ) if (!response.ok) { const text = await response.text().catch(() => '') @@ -146,7 +155,7 @@ async function fetchCsrf( } const token = response.headers.get('x-csrf-token') - const cookie = joinSetCookies(response.headers) + const cookie = joinSetCookies(response) if (!token) return null return { token, cookie } } @@ -217,12 +226,16 @@ async function callOdata( if (csrf.cookie) headers.Cookie = csrf.cookie } - const response = await fetch(url, { - method: req.method, - headers, - body: hasBody ? JSON.stringify(req.body) : undefined, - signal: AbortSignal.timeout(OUTBOUND_FETCH_TIMEOUT_MS), - }) + const response = await secureFetchWithValidation( + url, + { + method: req.method, + headers, + body: hasBody ? JSON.stringify(req.body) : undefined, + timeout: OUTBOUND_FETCH_TIMEOUT_MS, + }, + 'baseUrl' + ) const raw = await response.text() let parsed: unknown = null diff --git a/apps/sim/blocks/blocks/sap_concur.ts b/apps/sim/blocks/blocks/sap_concur.ts new file mode 100644 index 00000000000..54da09b093b --- /dev/null +++ b/apps/sim/blocks/blocks/sap_concur.ts @@ -0,0 +1,1920 @@ +import { SapConcurIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import { normalizeFileInput } from '@/blocks/utils' +import type { SapConcurProxyResponse, UserFileLike } from '@/tools/sap_concur/types' + +const toBool = (v: unknown): boolean | undefined => { + if (v === undefined || v === null || v === '') return undefined + if (typeof v === 'boolean') return v + if (typeof v === 'string') return v.toLowerCase() === 'true' + return Boolean(v) +} + +const REPORT_USER_OPS = [ + 'sap_concur_list_expense_reports', + 'sap_concur_get_expense_report', + 'sap_concur_create_expense_report', + 'sap_concur_update_expense_report', + 'sap_concur_submit_expense_report', + 'sap_concur_recall_expense_report', + 'sap_concur_list_expenses', + 'sap_concur_get_expense', + 'sap_concur_get_itemizations', + 'sap_concur_list_allocations', + 'sap_concur_get_allocation', + 'sap_concur_update_allocation', + 'sap_concur_list_attendee_associations', + 'sap_concur_associate_attendees', + 'sap_concur_remove_all_attendees', + 'sap_concur_list_report_comments', + 'sap_concur_create_report_comment', + 'sap_concur_list_exceptions', + 'sap_concur_create_quick_expense', + 'sap_concur_create_quick_expense_with_image', + 'sap_concur_list_receipts', + 'sap_concur_list_reports_to_approve', + 'sap_concur_upload_receipt_image', +] + +const REPORT_GET_CONTEXT_TYPE_OPS = ['sap_concur_get_expense_report'] + +const EXPENSE_READ_CONTEXT_TYPE_OPS = [ + 'sap_concur_list_expense_reports', + 'sap_concur_list_expenses', + 'sap_concur_get_expense', + 'sap_concur_get_itemizations', + 'sap_concur_list_report_comments', + 'sap_concur_list_exceptions', +] + +const QUICK_EXPENSE_CONTEXT_TYPE_OPS = [ + 'sap_concur_create_quick_expense', + 'sap_concur_create_quick_expense_with_image', +] + +const MANAGER_ONLY_CONTEXT_TYPE_OPS = ['sap_concur_list_reports_to_approve'] + +const ATTENDEE_CONTEXT_TYPE_OPS = [ + 'sap_concur_list_attendee_associations', + 'sap_concur_associate_attendees', + 'sap_concur_remove_all_attendees', + 'sap_concur_create_report_comment', +] + +const ALLOCATION_CONTEXT_TYPE_OPS = [ + 'sap_concur_list_allocations', + 'sap_concur_get_allocation', + 'sap_concur_update_allocation', + 'sap_concur_recall_expense_report', + 'sap_concur_create_expense_report', + 'sap_concur_update_expense_report', +] + +const REPORT_ID_OPS = [ + 'sap_concur_get_expense_report', + 'sap_concur_update_expense_report', + 'sap_concur_delete_expense_report', + 'sap_concur_submit_expense_report', + 'sap_concur_recall_expense_report', + 'sap_concur_approve_expense_report', + 'sap_concur_send_back_expense_report', + 'sap_concur_list_expenses', + 'sap_concur_get_expense', + 'sap_concur_get_itemizations', + 'sap_concur_update_expense', + 'sap_concur_delete_expense', + 'sap_concur_list_allocations', + 'sap_concur_get_allocation', + 'sap_concur_update_allocation', + 'sap_concur_list_attendee_associations', + 'sap_concur_associate_attendees', + 'sap_concur_remove_all_attendees', + 'sap_concur_list_report_comments', + 'sap_concur_create_report_comment', + 'sap_concur_list_exceptions', +] + +const EXPENSE_ID_OPS = [ + 'sap_concur_get_expense', + 'sap_concur_get_itemizations', + 'sap_concur_update_expense', + 'sap_concur_delete_expense', + 'sap_concur_list_allocations', + 'sap_concur_list_attendee_associations', + 'sap_concur_associate_attendees', + 'sap_concur_remove_all_attendees', +] + +const REQUEST_UUID_OPS = [ + 'sap_concur_get_travel_request', + 'sap_concur_update_travel_request', + 'sap_concur_delete_travel_request', + 'sap_concur_move_travel_request', + 'sap_concur_list_travel_request_comments', + 'sap_concur_create_expected_expense', + 'sap_concur_list_expected_expenses', +] + +const RECEIPT_UPLOAD_OPS = [ + 'sap_concur_upload_receipt_image', + 'sap_concur_create_quick_expense_with_image', +] + +const LIST_ITEM_ID_OPS = [ + 'sap_concur_get_list_item', + 'sap_concur_update_list_item', + 'sap_concur_delete_list_item', +] + +const BODY_OPS = [ + 'sap_concur_create_expense_report', + 'sap_concur_update_expense_report', + 'sap_concur_submit_expense_report', + 'sap_concur_recall_expense_report', + 'sap_concur_approve_expense_report', + 'sap_concur_send_back_expense_report', + 'sap_concur_update_expense', + 'sap_concur_update_allocation', + 'sap_concur_associate_attendees', + 'sap_concur_create_list_item', + 'sap_concur_create_quick_expense', + 'sap_concur_create_quick_expense_with_image', + 'sap_concur_create_travel_request', + 'sap_concur_update_list_item', + 'sap_concur_update_travel_request', + 'sap_concur_move_travel_request', + 'sap_concur_create_expected_expense', + 'sap_concur_update_expected_expense', + 'sap_concur_create_cash_advance', + 'sap_concur_issue_cash_advance', + 'sap_concur_create_user', + 'sap_concur_update_user', + 'sap_concur_search_users', + 'sap_concur_create_purchase_request', + 'sap_concur_get_exchange_rate', +] + +export const SapConcurBlock: BlockConfig = { + type: 'sap_concur', + name: 'SAP Concur', + description: 'Manage expense reports, travel requests, cash advances, and more in SAP Concur', + authMode: AuthMode.ApiKey, + longDescription: + 'Connect SAP Concur via OAuth 2.0. Manage expense reports and line items, allocations, attendees, comments, exceptions, quick expenses, receipts, travel requests and expected expenses, cash advances, itineraries, user identities, custom lists, budgets, exchange rates, and purchase requests across every Concur datacenter.', + docsLink: 'https://docs.sim.ai/tools/sap_concur', + category: 'tools', + integrationType: IntegrationType.Other, + tags: ['automation'], + bgColor: '#FFFFFF', + icon: SapConcurIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'List Expense Reports', id: 'sap_concur_list_expense_reports' }, + { label: 'Get Expense Report', id: 'sap_concur_get_expense_report' }, + { label: 'Create Expense Report', id: 'sap_concur_create_expense_report' }, + { label: 'Update Expense Report', id: 'sap_concur_update_expense_report' }, + { label: 'Delete Expense Report', id: 'sap_concur_delete_expense_report' }, + { label: 'Submit Expense Report', id: 'sap_concur_submit_expense_report' }, + { label: 'Recall Expense Report', id: 'sap_concur_recall_expense_report' }, + { label: 'Approve Expense Report', id: 'sap_concur_approve_expense_report' }, + { label: 'Send Back Expense Report', id: 'sap_concur_send_back_expense_report' }, + { label: 'List Reports To Approve', id: 'sap_concur_list_reports_to_approve' }, + { label: 'List Expenses', id: 'sap_concur_list_expenses' }, + { label: 'Get Expense', id: 'sap_concur_get_expense' }, + { label: 'Update Expense', id: 'sap_concur_update_expense' }, + { label: 'Delete Expense', id: 'sap_concur_delete_expense' }, + { label: 'Get Itemizations', id: 'sap_concur_get_itemizations' }, + { label: 'List Allocations', id: 'sap_concur_list_allocations' }, + { label: 'Get Allocation', id: 'sap_concur_get_allocation' }, + { label: 'Update Allocation', id: 'sap_concur_update_allocation' }, + { label: 'List Attendee Associations', id: 'sap_concur_list_attendee_associations' }, + { label: 'Associate Attendees', id: 'sap_concur_associate_attendees' }, + { label: 'Remove All Attendees', id: 'sap_concur_remove_all_attendees' }, + { label: 'List Report Comments', id: 'sap_concur_list_report_comments' }, + { label: 'Create Report Comment', id: 'sap_concur_create_report_comment' }, + { label: 'List Exceptions', id: 'sap_concur_list_exceptions' }, + { label: 'Create Quick Expense', id: 'sap_concur_create_quick_expense' }, + { + label: 'Create Quick Expense (With Image)', + id: 'sap_concur_create_quick_expense_with_image', + }, + { label: 'List Receipts', id: 'sap_concur_list_receipts' }, + { label: 'Get Receipt', id: 'sap_concur_get_receipt' }, + { label: 'Get Receipt Status', id: 'sap_concur_get_receipt_status' }, + { label: 'Upload Receipt Image', id: 'sap_concur_upload_receipt_image' }, + { label: 'List Travel Requests', id: 'sap_concur_list_travel_requests' }, + { label: 'Get Travel Request', id: 'sap_concur_get_travel_request' }, + { label: 'Create Travel Request', id: 'sap_concur_create_travel_request' }, + { label: 'Update Travel Request', id: 'sap_concur_update_travel_request' }, + { label: 'Delete Travel Request', id: 'sap_concur_delete_travel_request' }, + { label: 'Move Travel Request (Workflow Action)', id: 'sap_concur_move_travel_request' }, + { + label: 'List Travel Request Comments', + id: 'sap_concur_list_travel_request_comments', + }, + { + label: 'Get Request Cash Advance', + id: 'sap_concur_get_request_cash_advance', + }, + { label: 'Create Expected Expense', id: 'sap_concur_create_expected_expense' }, + { label: 'List Expected Expenses', id: 'sap_concur_list_expected_expenses' }, + { label: 'Get Expected Expense', id: 'sap_concur_get_expected_expense' }, + { label: 'Update Expected Expense', id: 'sap_concur_update_expected_expense' }, + { label: 'Delete Expected Expense', id: 'sap_concur_delete_expected_expense' }, + { label: 'Create Cash Advance', id: 'sap_concur_create_cash_advance' }, + { label: 'Get Cash Advance', id: 'sap_concur_get_cash_advance' }, + { label: 'Issue Cash Advance', id: 'sap_concur_issue_cash_advance' }, + { label: 'List Itineraries (Trips)', id: 'sap_concur_list_itineraries' }, + { label: 'Get Itinerary (Trip)', id: 'sap_concur_get_itinerary' }, + { label: 'List Users', id: 'sap_concur_list_users' }, + { label: 'Get User', id: 'sap_concur_get_user' }, + { label: 'Create User', id: 'sap_concur_create_user' }, + { label: 'Update User (PATCH)', id: 'sap_concur_update_user' }, + { label: 'Delete User', id: 'sap_concur_delete_user' }, + { label: 'Search Users', id: 'sap_concur_search_users' }, + { label: 'List Lists', id: 'sap_concur_list_lists' }, + { label: 'Get List', id: 'sap_concur_get_list' }, + { label: 'List List Items', id: 'sap_concur_list_list_items' }, + { label: 'Get List Item', id: 'sap_concur_get_list_item' }, + { label: 'Create List Item', id: 'sap_concur_create_list_item' }, + { label: 'Update List Item', id: 'sap_concur_update_list_item' }, + { label: 'Delete List Item', id: 'sap_concur_delete_list_item' }, + { label: 'List Budgets', id: 'sap_concur_list_budgets' }, + { label: 'Get Budget', id: 'sap_concur_get_budget' }, + { label: 'List Budget Categories', id: 'sap_concur_list_budget_categories' }, + { label: 'Get Exchange Rate', id: 'sap_concur_get_exchange_rate' }, + { label: 'Create Purchase Request', id: 'sap_concur_create_purchase_request' }, + { label: 'Get Purchase Request', id: 'sap_concur_get_purchase_request' }, + { label: 'Get Travel Profile', id: 'sap_concur_get_travel_profile' }, + { + label: 'List Travel Profiles Summary', + id: 'sap_concur_list_travel_profiles_summary', + }, + { label: 'Search Locations', id: 'sap_concur_search_locations' }, + ], + value: () => 'sap_concur_list_expense_reports', + required: true, + }, + + // Auth fields + { + id: 'datacenter', + title: 'Datacenter', + type: 'dropdown', + options: [ + { label: 'US (us.api.concursolutions.com)', id: 'us.api.concursolutions.com' }, + { label: 'US 2 (us2.api.concursolutions.com)', id: 'us2.api.concursolutions.com' }, + { label: 'EU (eu.api.concursolutions.com)', id: 'eu.api.concursolutions.com' }, + { label: 'EU 2 (eu2.api.concursolutions.com)', id: 'eu2.api.concursolutions.com' }, + { label: 'EMEA (emea.api.concursolutions.com)', id: 'emea.api.concursolutions.com' }, + { label: 'CN (cn.api.concursolutions.com)', id: 'cn.api.concursolutions.com' }, + ], + value: () => 'us.api.concursolutions.com', + required: true, + }, + { + id: 'grantType', + title: 'OAuth Grant Type', + type: 'dropdown', + options: [ + { label: 'Client Credentials', id: 'client_credentials' }, + { label: 'Password', id: 'password' }, + { label: 'Refresh Token', id: 'refresh_token' }, + ], + value: () => 'client_credentials', + }, + { + id: 'clientId', + title: 'OAuth Client ID', + type: 'short-input', + placeholder: 'Concur OAuth client ID', + password: true, + required: true, + }, + { + id: 'clientSecret', + title: 'OAuth Client Secret', + type: 'short-input', + placeholder: 'Concur OAuth client secret', + password: true, + required: true, + }, + { + id: 'username', + title: 'Username', + type: 'short-input', + placeholder: 'Username (password grant only)', + condition: { field: 'grantType', value: 'password' }, + required: { field: 'grantType', value: 'password' }, + }, + { + id: 'password', + title: 'Password', + type: 'short-input', + placeholder: 'Password (password grant only)', + password: true, + condition: { field: 'grantType', value: 'password' }, + required: { field: 'grantType', value: 'password' }, + }, + { + id: 'companyUuid', + title: 'Company UUID', + type: 'short-input', + placeholder: 'Multi-company access token UUID (optional)', + mode: 'advanced', + }, + + // Shared user/context fields for expense report ops + { + id: 'userId', + title: 'User ID', + type: 'short-input', + placeholder: 'Concur user UUID', + condition: { field: 'operation', value: REPORT_USER_OPS }, + required: { field: 'operation', value: REPORT_USER_OPS }, + }, + { + id: 'contextType', + title: 'Context Type', + type: 'dropdown', + options: [ + { label: 'TRAVELER', id: 'TRAVELER' }, + { label: 'MANAGER', id: 'MANAGER' }, + { label: 'PROCESSOR', id: 'PROCESSOR' }, + { label: 'PROXY', id: 'PROXY' }, + ], + value: () => 'TRAVELER', + condition: { field: 'operation', value: REPORT_GET_CONTEXT_TYPE_OPS }, + required: { field: 'operation', value: REPORT_GET_CONTEXT_TYPE_OPS }, + }, + { + id: 'contextType', + title: 'Context Type', + type: 'dropdown', + options: [ + { label: 'TRAVELER', id: 'TRAVELER' }, + { label: 'MANAGER', id: 'MANAGER' }, + { label: 'PROXY', id: 'PROXY' }, + ], + value: () => 'TRAVELER', + condition: { field: 'operation', value: EXPENSE_READ_CONTEXT_TYPE_OPS }, + required: { field: 'operation', value: EXPENSE_READ_CONTEXT_TYPE_OPS }, + }, + { + id: 'contextType', + title: 'Context Type', + type: 'dropdown', + options: [{ label: 'TRAVELER', id: 'TRAVELER' }], + value: () => 'TRAVELER', + condition: { field: 'operation', value: QUICK_EXPENSE_CONTEXT_TYPE_OPS }, + required: { field: 'operation', value: QUICK_EXPENSE_CONTEXT_TYPE_OPS }, + }, + { + id: 'contextType', + title: 'Context Type', + type: 'dropdown', + options: [ + { label: 'TRAVELER', id: 'TRAVELER' }, + { label: 'PROXY', id: 'PROXY' }, + ], + value: () => 'TRAVELER', + condition: { field: 'operation', value: ALLOCATION_CONTEXT_TYPE_OPS }, + required: { field: 'operation', value: ALLOCATION_CONTEXT_TYPE_OPS }, + }, + { + id: 'contextType', + title: 'Context Type', + type: 'dropdown', + options: [ + { label: 'TRAVELER', id: 'TRAVELER' }, + { label: 'PROXY', id: 'PROXY' }, + ], + value: () => 'TRAVELER', + condition: { field: 'operation', value: ATTENDEE_CONTEXT_TYPE_OPS }, + required: { field: 'operation', value: ATTENDEE_CONTEXT_TYPE_OPS }, + }, + { + id: 'contextType', + title: 'Context Type', + type: 'dropdown', + options: [{ label: 'MANAGER', id: 'MANAGER' }], + value: () => 'MANAGER', + condition: { field: 'operation', value: MANAGER_ONLY_CONTEXT_TYPE_OPS }, + required: { field: 'operation', value: MANAGER_ONLY_CONTEXT_TYPE_OPS }, + }, + + // Report ID + { + id: 'reportId', + title: 'Report ID', + type: 'short-input', + placeholder: 'Report ID', + condition: { field: 'operation', value: REPORT_ID_OPS }, + required: { field: 'operation', value: REPORT_ID_OPS }, + }, + { + id: 'expenseId', + title: 'Expense ID', + type: 'short-input', + placeholder: 'Expense entry ID', + condition: { field: 'operation', value: EXPENSE_ID_OPS }, + required: { field: 'operation', value: EXPENSE_ID_OPS }, + }, + { + id: 'allocationId', + title: 'Allocation ID', + type: 'short-input', + placeholder: 'Allocation ID', + condition: { + field: 'operation', + value: ['sap_concur_get_allocation', 'sap_concur_update_allocation'], + }, + required: { + field: 'operation', + value: ['sap_concur_get_allocation', 'sap_concur_update_allocation'], + }, + }, + { + id: 'expenseReportUser', + title: 'User', + type: 'short-input', + placeholder: 'Login ID or user identifier', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'approvalStatusCode', + title: 'Approval Status Code', + type: 'short-input', + placeholder: 'A_NOTF, A_PEND, A_APPR...', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'paymentStatusCode', + title: 'Payment Status Code', + type: 'short-input', + placeholder: 'P_NOTP, P_PAID...', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'currencyCode', + title: 'Currency Code', + type: 'short-input', + placeholder: 'USD, EUR...', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'approverLoginID', + title: 'Approver Login ID', + type: 'short-input', + placeholder: 'approver@example.com', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'submitDateAfter', + title: 'Submit Date After', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'submitDateBefore', + title: 'Submit Date Before', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'paidDateAfter', + title: 'Paid Date After', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'paidDateBefore', + title: 'Paid Date Before', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'modifiedDateAfter', + title: 'Modified Date After', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'modifiedDateBefore', + title: 'Modified Date Before', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'createDateAfter', + title: 'Create Date After', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'createDateBefore', + title: 'Create Date Before', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'comment', + title: 'Comment', + type: 'long-input', + placeholder: 'Comment text', + condition: { field: 'operation', value: 'sap_concur_create_report_comment' }, + required: { field: 'operation', value: 'sap_concur_create_report_comment' }, + }, + { + id: 'includeAllComments', + title: 'Include All Comments', + type: 'switch', + condition: { field: 'operation', value: 'sap_concur_list_report_comments' }, + mode: 'advanced', + }, + + // Receipt + { + id: 'receiptId', + title: 'Receipt ID', + type: 'short-input', + placeholder: 'Receipt ID', + condition: { + field: 'operation', + value: ['sap_concur_get_receipt', 'sap_concur_get_receipt_status'], + }, + required: { + field: 'operation', + value: ['sap_concur_get_receipt', 'sap_concur_get_receipt_status'], + }, + }, + + // Travel Requests + { + id: 'requestUuid', + title: 'Travel Request UUID', + type: 'short-input', + placeholder: 'Travel request UUID', + condition: { field: 'operation', value: REQUEST_UUID_OPS }, + required: { field: 'operation', value: REQUEST_UUID_OPS }, + }, + { + id: 'view', + title: 'View', + type: 'short-input', + placeholder: 'ALL, ACTIVE, PENDING, TOAPPROVE', + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + }, + { + id: 'travelRequestApprovedBefore', + title: 'Approved Before', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + mode: 'advanced', + }, + { + id: 'travelRequestApprovedAfter', + title: 'Approved After', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + mode: 'advanced', + }, + { + id: 'travelRequestModifiedBefore', + title: 'Modified Before', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + mode: 'advanced', + }, + { + id: 'travelRequestModifiedAfter', + title: 'Modified After', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + mode: 'advanced', + }, + { + id: 'travelRequestSortField', + title: 'Sort Field', + type: 'short-input', + placeholder: 'startDate', + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + mode: 'advanced', + }, + { + id: 'travelRequestSortOrder', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Ascending', id: 'asc' }, + { label: 'Descending', id: 'desc' }, + ], + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + mode: 'advanced', + }, + { + id: 'travelRequestUserId', + title: 'User ID', + type: 'short-input', + placeholder: 'Concur user UUID (optional impersonation)', + condition: { + field: 'operation', + value: [ + 'sap_concur_list_travel_requests', + 'sap_concur_get_travel_request', + 'sap_concur_create_travel_request', + 'sap_concur_delete_travel_request', + 'sap_concur_move_travel_request', + ], + }, + mode: 'advanced', + }, + { + id: 'action', + title: 'Workflow Action', + type: 'dropdown', + options: [ + { label: 'Submit', id: 'submit' }, + { label: 'Recall', id: 'recall' }, + { label: 'Cancel', id: 'cancel' }, + { label: 'Approve', id: 'approve' }, + { label: 'Send Back', id: 'sendback' }, + { label: 'Close', id: 'close' }, + { label: 'Reopen', id: 'reopen' }, + ], + value: () => 'submit', + condition: { field: 'operation', value: 'sap_concur_move_travel_request' }, + required: { field: 'operation', value: 'sap_concur_move_travel_request' }, + }, + + // Expected Expenses + { + id: 'expectedExpenseUserId', + title: 'User ID', + type: 'short-input', + placeholder: 'Concur user UUID (optional impersonation)', + condition: { + field: 'operation', + value: [ + 'sap_concur_list_expected_expenses', + 'sap_concur_create_expected_expense', + 'sap_concur_get_expected_expense', + 'sap_concur_update_expected_expense', + 'sap_concur_delete_expected_expense', + ], + }, + mode: 'advanced', + }, + { + id: 'expenseUuid', + title: 'Expected Expense UUID', + type: 'short-input', + placeholder: 'Expected expense UUID', + condition: { + field: 'operation', + value: [ + 'sap_concur_get_expected_expense', + 'sap_concur_update_expected_expense', + 'sap_concur_delete_expected_expense', + ], + }, + required: { + field: 'operation', + value: [ + 'sap_concur_get_expected_expense', + 'sap_concur_update_expected_expense', + 'sap_concur_delete_expected_expense', + ], + }, + }, + + // Cash advances + { + id: 'cashAdvanceUuid', + title: 'Cash Advance UUID', + type: 'short-input', + placeholder: 'Cash advance UUID', + condition: { field: 'operation', value: 'sap_concur_get_request_cash_advance' }, + required: { field: 'operation', value: 'sap_concur_get_request_cash_advance' }, + }, + { + id: 'cashAdvanceId', + title: 'Cash Advance ID', + type: 'short-input', + placeholder: 'Cash advance ID', + condition: { + field: 'operation', + value: ['sap_concur_get_cash_advance', 'sap_concur_issue_cash_advance'], + }, + required: { + field: 'operation', + value: ['sap_concur_get_cash_advance', 'sap_concur_issue_cash_advance'], + }, + }, + + // Itineraries + { + id: 'tripId', + title: 'Trip ID', + type: 'short-input', + placeholder: 'Trip ID', + condition: { field: 'operation', value: 'sap_concur_get_itinerary' }, + required: { field: 'operation', value: 'sap_concur_get_itinerary' }, + }, + { + id: 'useridType', + title: 'User ID Type', + type: 'dropdown', + options: [ + { label: 'Default', id: '' }, + { label: 'login', id: 'login' }, + { label: 'xmlsyncid', id: 'xmlsyncid' }, + { label: 'uuid', id: 'uuid' }, + ], + value: () => '', + condition: { + field: 'operation', + value: [ + 'sap_concur_get_itinerary', + 'sap_concur_list_itineraries', + 'sap_concur_get_travel_profile', + ], + }, + mode: 'advanced', + }, + { + id: 'useridValue', + title: 'User ID Value', + type: 'short-input', + placeholder: 'User identifier value', + condition: { + field: 'operation', + value: [ + 'sap_concur_get_itinerary', + 'sap_concur_list_itineraries', + 'sap_concur_get_travel_profile', + ], + }, + mode: 'advanced', + }, + { + id: 'systemFormat', + title: 'System Format', + type: 'short-input', + placeholder: 'GDS', + condition: { field: 'operation', value: 'sap_concur_get_itinerary' }, + mode: 'advanced', + }, + { + id: 'startDate', + title: 'Start Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + }, + { + id: 'endDate', + title: 'End Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + }, + { + id: 'bookingType', + title: 'Booking Type', + type: 'short-input', + placeholder: 'air, car, hotel, rail', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + mode: 'advanced', + }, + { + id: 'itineraryItemsPerPage', + title: 'Items Per Page', + type: 'short-input', + placeholder: '25', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + }, + { + id: 'itineraryPage', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + }, + { + id: 'includeMetadata', + title: 'Include Metadata', + type: 'switch', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + mode: 'advanced', + }, + { + id: 'includeCanceledTrips', + title: 'Include Canceled Trips', + type: 'switch', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + mode: 'advanced', + }, + { + id: 'createdAfterDate', + title: 'Created After Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + mode: 'advanced', + }, + { + id: 'createdBeforeDate', + title: 'Created Before Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + mode: 'advanced', + }, + { + id: 'itineraryLastModifiedDate', + title: 'Last Modified Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + mode: 'advanced', + }, + + // Users + { + id: 'userUuid', + title: 'User UUID', + type: 'short-input', + placeholder: 'User UUID', + condition: { + field: 'operation', + value: ['sap_concur_get_user', 'sap_concur_update_user', 'sap_concur_delete_user'], + }, + required: { + field: 'operation', + value: ['sap_concur_get_user', 'sap_concur_update_user', 'sap_concur_delete_user'], + }, + }, + { + id: 'count', + title: 'Count', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: 'sap_concur_list_users' }, + }, + { + id: 'usersCursor', + title: 'Cursor', + type: 'short-input', + placeholder: 'Pagination cursor from previous response', + condition: { field: 'operation', value: 'sap_concur_list_users' }, + }, + { + id: 'attributes', + title: 'Attributes', + type: 'short-input', + placeholder: 'id,active,emails', + condition: { + field: 'operation', + value: ['sap_concur_list_users', 'sap_concur_get_user'], + }, + }, + { + id: 'excludedAttributes', + title: 'Excluded Attributes', + type: 'short-input', + placeholder: 'name,emails', + condition: { + field: 'operation', + value: ['sap_concur_list_users', 'sap_concur_get_user'], + }, + mode: 'advanced', + }, + + // Lists + { + id: 'listId', + title: 'List ID', + type: 'short-input', + placeholder: 'List ID', + condition: { + field: 'operation', + value: ['sap_concur_get_list', 'sap_concur_list_list_items', 'sap_concur_get_list_item'], + }, + required: { + field: 'operation', + value: ['sap_concur_get_list', 'sap_concur_list_list_items'], + }, + }, + // Budgets + { + id: 'budgetId', + title: 'Budget Item Header ID', + type: 'short-input', + placeholder: 'Budget header syncguid', + condition: { field: 'operation', value: 'sap_concur_get_budget' }, + required: { field: 'operation', value: 'sap_concur_get_budget' }, + }, + { + id: 'adminView', + title: 'Admin View', + type: 'switch', + condition: { field: 'operation', value: 'sap_concur_list_budgets' }, + }, + { + id: 'responseSchema', + title: 'Response Schema', + type: 'dropdown', + options: [ + { label: 'Default', id: '' }, + { label: 'Compact', id: 'COMPACT' }, + ], + value: () => '', + condition: { field: 'operation', value: 'sap_concur_list_budgets' }, + }, + + // Purchase Requests + { + id: 'purchaseRequestId', + title: 'Purchase Request ID', + type: 'short-input', + placeholder: 'Purchase request ID', + condition: { field: 'operation', value: 'sap_concur_get_purchase_request' }, + required: { field: 'operation', value: 'sap_concur_get_purchase_request' }, + }, + + // Pagination (shared across many list ops) + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '25', + condition: { + field: 'operation', + value: ['sap_concur_list_expense_reports', 'sap_concur_list_travel_requests'], + }, + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: '0', + condition: { + field: 'operation', + value: ['sap_concur_list_budgets', 'sap_concur_list_expense_reports'], + }, + }, + { + id: 'page', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { + field: 'operation', + value: ['sap_concur_list_lists', 'sap_concur_list_list_items'], + }, + }, + { + id: 'sortBy', + title: 'Sort By', + type: 'short-input', + placeholder: 'name', + condition: { + field: 'operation', + value: ['sap_concur_list_lists', 'sap_concur_list_list_items'], + }, + mode: 'advanced', + }, + { + id: 'sortDirection', + title: 'Sort Direction', + type: 'dropdown', + options: [ + { label: 'Ascending', id: 'asc' }, + { label: 'Descending', id: 'desc' }, + ], + condition: { + field: 'operation', + value: ['sap_concur_list_lists', 'sap_concur_list_list_items'], + }, + mode: 'advanced', + }, + { + id: 'reportsToApproveSort', + title: 'Sort By', + type: 'short-input', + placeholder: 'reportDate', + condition: { field: 'operation', value: 'sap_concur_list_reports_to_approve' }, + mode: 'advanced', + }, + { + id: 'reportsToApproveOrder', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Ascending', id: 'asc' }, + { label: 'Descending', id: 'desc' }, + ], + condition: { field: 'operation', value: 'sap_concur_list_reports_to_approve' }, + mode: 'advanced', + }, + { + id: 'includeDelegateApprovals', + title: 'Include Delegate Approvals', + type: 'switch', + condition: { field: 'operation', value: 'sap_concur_list_reports_to_approve' }, + mode: 'advanced', + }, + { + id: 'start', + title: 'Start', + type: 'short-input', + placeholder: '0', + condition: { + field: 'operation', + value: ['sap_concur_list_travel_requests'], + }, + }, + + // List Item ID (for update/delete list item) + { + id: 'itemId', + title: 'List Item ID', + type: 'short-input', + placeholder: 'List item UUID', + condition: { field: 'operation', value: LIST_ITEM_ID_OPS }, + required: { field: 'operation', value: LIST_ITEM_ID_OPS }, + }, + + // Travel Profile fields + { + id: 'lastModifiedDate', + title: 'Last Modified Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD or 1900-01-01T12:00:00', + condition: { + field: 'operation', + value: 'sap_concur_list_travel_profiles_summary', + }, + required: { + field: 'operation', + value: 'sap_concur_list_travel_profiles_summary', + }, + }, + { + id: 'travelProfilePage', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: 'sap_concur_list_travel_profiles_summary' }, + }, + { + id: 'itemsPerPage', + title: 'Items Per Page', + type: 'short-input', + placeholder: '200', + condition: { field: 'operation', value: 'sap_concur_list_travel_profiles_summary' }, + }, + { + id: 'travelProfileActive', + title: 'Active Filter', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'Active', id: 'Active' }, + { label: 'Inactive', id: 'Inactive' }, + ], + value: () => '', + condition: { field: 'operation', value: 'sap_concur_list_travel_profiles_summary' }, + }, + { + id: 'travelConfigs', + title: 'Travel Config IDs', + type: 'short-input', + placeholder: 'Comma-separated config ids', + condition: { field: 'operation', value: 'sap_concur_list_travel_profiles_summary' }, + }, + + // Locations fields (v5) + { + id: 'searchText', + title: 'Search Text', + type: 'short-input', + placeholder: 'Free-text search (city, landmark, etc.)', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + }, + { + id: 'locCode', + title: 'Location Code', + type: 'short-input', + placeholder: 'IATA / city code (e.g., SEA)', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + }, + { + id: 'locationNameId', + title: 'Location Name ID', + type: 'short-input', + placeholder: 'Concur location name id', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + mode: 'advanced', + }, + { + id: 'locationNameKey', + title: 'Location Name Key', + type: 'short-input', + placeholder: 'Concur location name key', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + mode: 'advanced', + }, + { + id: 'countryCode', + title: 'Country Code (ISO 3166-1)', + type: 'short-input', + placeholder: 'US', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + }, + { + id: 'subdivisionCode', + title: 'Subdivision Code (ISO 3166-2)', + type: 'short-input', + placeholder: 'US-WA', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + }, + { + id: 'adminRegionId', + title: 'Administrative Region ID', + type: 'short-input', + placeholder: 'Concur admin region id', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + mode: 'advanced', + }, + + // Receipt Image (basic mode — file picker) + { + id: 'receiptFile', + title: 'Receipt Image', + type: 'file-upload', + canonicalParamId: 'receipt', + placeholder: 'Upload receipt image', + condition: { field: 'operation', value: RECEIPT_UPLOAD_OPS }, + mode: 'basic', + multiple: false, + required: true, + acceptedTypes: 'image/jpeg,image/png,image/gif,image/tiff,application/pdf', + }, + // Receipt Image (advanced mode — variable reference) + { + id: 'receiptFileRef', + title: 'Receipt Image', + type: 'short-input', + canonicalParamId: 'receipt', + placeholder: 'Reference file from previous block', + condition: { field: 'operation', value: RECEIPT_UPLOAD_OPS }, + mode: 'advanced', + required: true, + }, + { + id: 'forwardId', + title: 'Forward ID', + type: 'short-input', + placeholder: 'Optional dedup id (max 40 chars)', + condition: { field: 'operation', value: 'sap_concur_upload_receipt_image' }, + mode: 'advanced', + }, + + // Body (JSON payload) — shared across all create/update/action ops + { + id: 'body', + title: 'Request Body (JSON)', + type: 'long-input', + placeholder: '{ ... }', + condition: { field: 'operation', value: BODY_OPS }, + required: { + field: 'operation', + value: [ + 'sap_concur_create_expense_report', + 'sap_concur_update_expense_report', + 'sap_concur_approve_expense_report', + 'sap_concur_send_back_expense_report', + 'sap_concur_update_expense', + 'sap_concur_update_allocation', + 'sap_concur_associate_attendees', + 'sap_concur_create_quick_expense', + 'sap_concur_create_quick_expense_with_image', + 'sap_concur_create_travel_request', + 'sap_concur_update_travel_request', + 'sap_concur_create_expected_expense', + 'sap_concur_update_expected_expense', + 'sap_concur_create_cash_advance', + 'sap_concur_create_user', + 'sap_concur_update_user', + 'sap_concur_search_users', + 'sap_concur_create_purchase_request', + 'sap_concur_get_exchange_rate', + 'sap_concur_create_list_item', + 'sap_concur_update_list_item', + ], + }, + }, + ], + tools: { + access: [ + 'sap_concur_approve_expense_report', + 'sap_concur_associate_attendees', + 'sap_concur_create_cash_advance', + 'sap_concur_create_expected_expense', + 'sap_concur_create_expense_report', + 'sap_concur_create_list_item', + 'sap_concur_create_purchase_request', + 'sap_concur_create_quick_expense', + 'sap_concur_create_quick_expense_with_image', + 'sap_concur_create_report_comment', + 'sap_concur_create_travel_request', + 'sap_concur_create_user', + 'sap_concur_delete_expected_expense', + 'sap_concur_delete_expense', + 'sap_concur_delete_expense_report', + 'sap_concur_delete_list_item', + 'sap_concur_delete_travel_request', + 'sap_concur_delete_user', + 'sap_concur_get_allocation', + 'sap_concur_get_budget', + 'sap_concur_get_cash_advance', + 'sap_concur_get_exchange_rate', + 'sap_concur_get_expected_expense', + 'sap_concur_get_expense', + 'sap_concur_get_expense_report', + 'sap_concur_get_itemizations', + 'sap_concur_get_itinerary', + 'sap_concur_get_list', + 'sap_concur_get_list_item', + 'sap_concur_get_purchase_request', + 'sap_concur_get_receipt', + 'sap_concur_get_receipt_status', + 'sap_concur_get_travel_profile', + 'sap_concur_get_travel_request', + 'sap_concur_get_user', + 'sap_concur_issue_cash_advance', + 'sap_concur_list_allocations', + 'sap_concur_list_attendee_associations', + 'sap_concur_list_budget_categories', + 'sap_concur_list_budgets', + 'sap_concur_list_exceptions', + 'sap_concur_list_expected_expenses', + 'sap_concur_list_expenses', + 'sap_concur_list_expense_reports', + 'sap_concur_list_itineraries', + 'sap_concur_list_lists', + 'sap_concur_list_list_items', + 'sap_concur_list_receipts', + 'sap_concur_list_report_comments', + 'sap_concur_list_reports_to_approve', + 'sap_concur_get_request_cash_advance', + 'sap_concur_list_travel_profiles_summary', + 'sap_concur_list_travel_request_comments', + 'sap_concur_list_travel_requests', + 'sap_concur_list_users', + 'sap_concur_move_travel_request', + 'sap_concur_recall_expense_report', + 'sap_concur_remove_all_attendees', + 'sap_concur_search_locations', + 'sap_concur_search_users', + 'sap_concur_send_back_expense_report', + 'sap_concur_submit_expense_report', + 'sap_concur_update_allocation', + 'sap_concur_update_expected_expense', + 'sap_concur_update_expense', + 'sap_concur_update_expense_report', + 'sap_concur_update_list_item', + 'sap_concur_update_travel_request', + 'sap_concur_update_user', + 'sap_concur_upload_receipt_image', + ], + config: { + tool: (params) => params.operation, + params: (params) => { + const auth = { + datacenter: params.datacenter || undefined, + grantType: params.grantType || undefined, + clientId: params.clientId, + clientSecret: params.clientSecret, + username: params.username || undefined, + password: params.password || undefined, + companyUuid: params.companyUuid || undefined, + } + + const limit = params.limit ? Number(params.limit) : undefined + const offset = params.offset ? Number(params.offset) : undefined + const start = params.start ? Number(params.start) : undefined + const count = params.count ? Number(params.count) : undefined + const startIndex = params.startIndex ? Number(params.startIndex) : undefined + const page = params.page ? Number(params.page) : undefined + const levelCount = params.levelCount ? Number(params.levelCount) : undefined + + switch (params.operation) { + case 'sap_concur_list_expense_reports': + return { + ...auth, + user: params.expenseReportUser || params.userId || undefined, + submitDateBefore: params.submitDateBefore || undefined, + submitDateAfter: params.submitDateAfter || undefined, + paidDateBefore: params.paidDateBefore || undefined, + paidDateAfter: params.paidDateAfter || undefined, + modifiedDateBefore: params.modifiedDateBefore || undefined, + modifiedDateAfter: params.modifiedDateAfter || undefined, + createDateBefore: params.createDateBefore || undefined, + createDateAfter: params.createDateAfter || undefined, + approvalStatusCode: params.approvalStatusCode || undefined, + paymentStatusCode: params.paymentStatusCode || undefined, + currencyCode: params.currencyCode || undefined, + approverLoginID: params.approverLoginID || undefined, + limit, + offset: params.offset ? String(params.offset) : undefined, + } + case 'sap_concur_get_expense_report': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + } + case 'sap_concur_create_expense_report': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + body: params.body, + } + case 'sap_concur_update_expense_report': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + body: params.body, + } + case 'sap_concur_delete_expense_report': + return { ...auth, reportId: params.reportId } + case 'sap_concur_submit_expense_report': + return { + ...auth, + userId: params.userId, + reportId: params.reportId, + body: params.body || undefined, + } + case 'sap_concur_recall_expense_report': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + body: params.body || undefined, + } + case 'sap_concur_approve_expense_report': + case 'sap_concur_send_back_expense_report': + return { + ...auth, + reportId: params.reportId, + body: params.body || undefined, + } + case 'sap_concur_list_reports_to_approve': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + sort: params.reportsToApproveSort || undefined, + order: params.reportsToApproveOrder || undefined, + includeDelegateApprovals: toBool(params.includeDelegateApprovals), + } + case 'sap_concur_list_expenses': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + } + case 'sap_concur_get_expense': + case 'sap_concur_get_itemizations': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + expenseId: params.expenseId, + } + case 'sap_concur_update_expense': + return { + ...auth, + reportId: params.reportId, + expenseId: params.expenseId, + body: params.body, + } + case 'sap_concur_delete_expense': + return { + ...auth, + reportId: params.reportId, + expenseId: params.expenseId, + } + case 'sap_concur_list_allocations': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + expenseId: params.expenseId, + } + case 'sap_concur_get_allocation': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + allocationId: params.allocationId, + } + case 'sap_concur_update_allocation': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + allocationId: params.allocationId, + body: params.body, + } + case 'sap_concur_list_attendee_associations': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + expenseId: params.expenseId, + } + case 'sap_concur_associate_attendees': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + expenseId: params.expenseId, + body: params.body, + } + case 'sap_concur_remove_all_attendees': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + expenseId: params.expenseId, + } + case 'sap_concur_list_report_comments': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + includeAllComments: toBool(params.includeAllComments), + } + case 'sap_concur_create_report_comment': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + comment: params.comment, + } + case 'sap_concur_list_exceptions': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + } + case 'sap_concur_create_quick_expense': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + body: params.body, + } + case 'sap_concur_list_receipts': + return { ...auth, userId: params.userId } + case 'sap_concur_get_receipt': + case 'sap_concur_get_receipt_status': + return { ...auth, receiptId: params.receiptId } + case 'sap_concur_list_travel_requests': + return { + ...auth, + view: params.view || undefined, + limit, + start, + userId: params.travelRequestUserId || undefined, + approvedBefore: params.travelRequestApprovedBefore || undefined, + approvedAfter: params.travelRequestApprovedAfter || undefined, + modifiedBefore: params.travelRequestModifiedBefore || undefined, + modifiedAfter: params.travelRequestModifiedAfter || undefined, + sortField: params.travelRequestSortField || undefined, + sortOrder: + params.travelRequestSortOrder === 'asc' || params.travelRequestSortOrder === 'desc' + ? params.travelRequestSortOrder + : undefined, + } + case 'sap_concur_get_travel_request': + case 'sap_concur_delete_travel_request': + return { + ...auth, + requestUuid: params.requestUuid, + userId: params.travelRequestUserId || undefined, + } + case 'sap_concur_create_travel_request': + return { ...auth, body: params.body, userId: params.travelRequestUserId || undefined } + case 'sap_concur_update_travel_request': + return { ...auth, requestUuid: params.requestUuid, body: params.body } + case 'sap_concur_move_travel_request': + return { + ...auth, + requestUuid: params.requestUuid, + action: params.action, + body: params.body || undefined, + userId: params.travelRequestUserId || undefined, + } + case 'sap_concur_list_travel_request_comments': + return { ...auth, requestUuid: params.requestUuid } + case 'sap_concur_list_expected_expenses': + return { + ...auth, + requestUuid: params.requestUuid, + userId: params.expectedExpenseUserId || undefined, + } + case 'sap_concur_get_request_cash_advance': + return { ...auth, cashAdvanceUuid: params.cashAdvanceUuid } + case 'sap_concur_create_expected_expense': + return { + ...auth, + requestUuid: params.requestUuid, + body: params.body, + userId: params.expectedExpenseUserId || undefined, + } + case 'sap_concur_get_expected_expense': + case 'sap_concur_delete_expected_expense': + return { + ...auth, + expenseUuid: params.expenseUuid, + userId: params.expectedExpenseUserId || undefined, + } + case 'sap_concur_update_expected_expense': + return { + ...auth, + expenseUuid: params.expenseUuid, + body: params.body, + userId: params.expectedExpenseUserId || undefined, + } + case 'sap_concur_create_cash_advance': + return { ...auth, body: params.body } + case 'sap_concur_get_cash_advance': + return { ...auth, cashAdvanceId: params.cashAdvanceId } + case 'sap_concur_issue_cash_advance': + return { + ...auth, + cashAdvanceId: params.cashAdvanceId, + body: params.body || undefined, + } + case 'sap_concur_list_itineraries': + return { + ...auth, + startDate: params.startDate || undefined, + endDate: params.endDate || undefined, + bookingType: params.bookingType || undefined, + useridType: params.useridType || undefined, + useridValue: params.useridValue || undefined, + itemsPerPage: params.itineraryItemsPerPage + ? Number(params.itineraryItemsPerPage) + : undefined, + page: params.itineraryPage ? Number(params.itineraryPage) : undefined, + includeMetadata: toBool(params.includeMetadata), + includeCanceledTrips: toBool(params.includeCanceledTrips), + createdAfterDate: params.createdAfterDate || undefined, + createdBeforeDate: params.createdBeforeDate || undefined, + lastModifiedDate: params.itineraryLastModifiedDate || undefined, + } + case 'sap_concur_get_itinerary': + return { + ...auth, + tripId: params.tripId, + useridType: params.useridType || undefined, + useridValue: params.useridValue || undefined, + systemFormat: params.systemFormat || undefined, + } + case 'sap_concur_list_users': + return { + ...auth, + count, + cursor: params.usersCursor || undefined, + attributes: params.attributes || undefined, + excludedAttributes: params.excludedAttributes || undefined, + } + case 'sap_concur_get_user': + return { + ...auth, + userUuid: params.userUuid, + attributes: params.attributes || undefined, + excludedAttributes: params.excludedAttributes || undefined, + } + case 'sap_concur_delete_user': + return { ...auth, userUuid: params.userUuid } + case 'sap_concur_create_user': + return { ...auth, body: params.body } + case 'sap_concur_update_user': + return { ...auth, userUuid: params.userUuid, body: params.body } + case 'sap_concur_search_users': + return { ...auth, body: params.body } + case 'sap_concur_list_lists': + return { + ...auth, + page, + sortBy: params.sortBy || undefined, + sortDirection: params.sortDirection || undefined, + value: params.value || undefined, + categoryType: params.categoryType || undefined, + isDeleted: toBool(params.isDeleted), + levelCount, + } + case 'sap_concur_get_list': + return { ...auth, listId: params.listId } + case 'sap_concur_list_list_items': + return { + ...auth, + listId: params.listId, + page, + sortBy: params.sortBy || undefined, + sortDirection: params.sortDirection || undefined, + hasChildren: toBool(params.hasChildren), + isDeleted: toBool(params.isDeleted), + shortCode: params.shortCode || undefined, + value: params.value || undefined, + shortCodeOrValue: params.shortCodeOrValue || undefined, + } + case 'sap_concur_get_list_item': + return { + ...auth, + listId: params.listId || undefined, + itemId: params.itemId, + } + case 'sap_concur_list_budgets': + return { + ...auth, + adminView: toBool(params.adminView), + offset, + responseSchema: params.responseSchema || undefined, + } + case 'sap_concur_get_budget': + return { ...auth, budgetId: params.budgetId } + case 'sap_concur_list_budget_categories': + return { ...auth } + case 'sap_concur_get_exchange_rate': + return { ...auth, body: params.body } + case 'sap_concur_create_purchase_request': + return { ...auth, body: params.body } + case 'sap_concur_get_purchase_request': + return { ...auth, purchaseRequestId: params.purchaseRequestId } + case 'sap_concur_create_list_item': + return { ...auth, body: params.body } + case 'sap_concur_update_list_item': + return { ...auth, itemId: params.itemId, body: params.body } + case 'sap_concur_delete_list_item': + return { ...auth, itemId: params.itemId } + case 'sap_concur_get_travel_profile': + return { + ...auth, + useridType: params.useridType || undefined, + useridValue: params.useridValue || undefined, + } + case 'sap_concur_list_travel_profiles_summary': + return { + ...auth, + lastModifiedDate: params.lastModifiedDate, + page: params.travelProfilePage ? Number(params.travelProfilePage) : undefined, + itemsPerPage: params.itemsPerPage ? Number(params.itemsPerPage) : undefined, + active: + params.travelProfileActive === 'Active' || params.travelProfileActive === 'Inactive' + ? params.travelProfileActive + : undefined, + travelConfigs: params.travelConfigs || undefined, + } + case 'sap_concur_search_locations': + return { + ...auth, + searchText: params.searchText || undefined, + locCode: params.locCode || undefined, + locationNameId: params.locationNameId || undefined, + locationNameKey: params.locationNameKey ? Number(params.locationNameKey) : undefined, + countryCode: params.countryCode || undefined, + subdivisionCode: params.subdivisionCode || undefined, + adminRegionId: params.adminRegionId || undefined, + } + case 'sap_concur_upload_receipt_image': { + const normalizedReceipt = normalizeFileInput(params.receipt, { single: true }) as + | UserFileLike + | undefined + return { + ...auth, + userId: params.userId, + receipt: normalizedReceipt, + forwardId: params.forwardId || undefined, + } + } + case 'sap_concur_create_quick_expense_with_image': { + const normalizedReceipt = normalizeFileInput(params.receipt, { single: true }) as + | UserFileLike + | undefined + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + receipt: normalizedReceipt, + body: params.body, + } + } + default: + throw new Error(`Unsupported SAP Concur operation: ${params.operation}`) + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + datacenter: { type: 'string', description: 'Concur datacenter base URL' }, + grantType: { type: 'string', description: 'OAuth grant type' }, + clientId: { type: 'string', description: 'OAuth client ID' }, + clientSecret: { type: 'string', description: 'OAuth client secret' }, + username: { type: 'string', description: 'Username (password grant only)' }, + password: { type: 'string', description: 'Password (password grant only)' }, + companyUuid: { type: 'string', description: 'Company UUID for multi-company tokens' }, + userId: { type: 'string', description: 'Concur user UUID' }, + contextType: { + type: 'string', + description: 'Access context (TRAVELER/MANAGER, or TRAVELER/PROXY for allocations)', + }, + reportId: { type: 'string', description: 'Expense report ID' }, + expenseId: { type: 'string', description: 'Expense entry ID' }, + allocationId: { type: 'string', description: 'Allocation ID' }, + expenseReportUser: { + type: 'string', + description: 'v3 list expense reports — user filter (login id)', + }, + submitDateBefore: { + type: 'string', + description: 'v3 list expense reports — submit date before', + }, + submitDateAfter: { type: 'string', description: 'v3 list expense reports — submit date after' }, + paidDateBefore: { type: 'string', description: 'v3 list expense reports — paid date before' }, + paidDateAfter: { type: 'string', description: 'v3 list expense reports — paid date after' }, + modifiedDateBefore: { + type: 'string', + description: 'v3 list expense reports — modified date before', + }, + modifiedDateAfter: { + type: 'string', + description: 'v3 list expense reports — modified date after', + }, + createDateBefore: { + type: 'string', + description: 'v3 list expense reports — create date before', + }, + createDateAfter: { + type: 'string', + description: 'v3 list expense reports — create date after', + }, + approvalStatusCode: { + type: 'string', + description: 'v3 list expense reports — approval status code', + }, + paymentStatusCode: { + type: 'string', + description: 'v3 list expense reports — payment status code', + }, + currencyCode: { type: 'string', description: 'v3 list expense reports — currency code' }, + approverLoginID: { type: 'string', description: 'v3 list expense reports — approver login id' }, + comment: { type: 'string', description: 'Comment text' }, + receiptId: { type: 'string', description: 'Receipt image ID' }, + requestUuid: { type: 'string', description: 'Travel request UUID' }, + view: { type: 'string', description: 'Travel request view filter' }, + travelRequestUserId: { + type: 'string', + description: 'User UUID for travel request impersonation/filter', + }, + travelRequestApprovedBefore: { type: 'string', description: 'Travel requests approved before' }, + travelRequestApprovedAfter: { type: 'string', description: 'Travel requests approved after' }, + travelRequestModifiedBefore: { type: 'string', description: 'Travel requests modified before' }, + travelRequestModifiedAfter: { type: 'string', description: 'Travel requests modified after' }, + travelRequestSortField: { type: 'string', description: 'Travel requests sort field' }, + travelRequestSortOrder: { type: 'string', description: 'Travel requests sort order' }, + action: { type: 'string', description: 'Travel request workflow action' }, + expectedExpenseUserId: { + type: 'string', + description: 'Expected expense impersonation user UUID', + }, + expenseUuid: { type: 'string', description: 'Expected expense UUID' }, + cashAdvanceId: { type: 'string', description: 'Cash advance ID' }, + cashAdvanceUuid: { type: 'string', description: 'Cash advance UUID (travel request scope)' }, + tripId: { type: 'string', description: 'Trip/itinerary ID' }, + startDate: { type: 'string', description: 'Itinerary start date filter' }, + endDate: { type: 'string', description: 'Itinerary end date filter' }, + bookingType: { type: 'string', description: 'Itinerary booking type filter' }, + systemFormat: { type: 'string', description: 'Itinerary system format (e.g., GDS)' }, + itineraryItemsPerPage: { type: 'number', description: 'Itinerary items per page' }, + itineraryPage: { type: 'number', description: 'Itinerary page number' }, + includeMetadata: { type: 'boolean', description: 'Include itinerary paging metadata' }, + includeCanceledTrips: { type: 'boolean', description: 'Include canceled trips' }, + createdAfterDate: { type: 'string', description: 'Itinerary created-after date' }, + createdBeforeDate: { type: 'string', description: 'Itinerary created-before date' }, + itineraryLastModifiedDate: { type: 'string', description: 'Itinerary last-modified date' }, + userUuid: { type: 'string', description: 'User identity UUID' }, + count: { type: 'number', description: 'SCIM count' }, + usersCursor: { type: 'string', description: 'SCIM v4.1 cursor for /users' }, + attributes: { type: 'string', description: 'SCIM attributes filter' }, + excludedAttributes: { type: 'string', description: 'SCIM excluded attributes' }, + listId: { type: 'string', description: 'Custom list ID' }, + itemId: { type: 'string', description: 'List item v4 UUID' }, + sortBy: { type: 'string', description: 'Sort field for v4 lists/items endpoints' }, + sortDirection: { type: 'string', description: 'Sort direction: asc or desc' }, + value: { type: 'string', description: 'Filter by value/name for v4 lists/items endpoints' }, + categoryType: { type: 'string', description: 'List category.type filter' }, + isDeleted: { type: 'boolean', description: 'Include deleted lists/items' }, + levelCount: { type: 'number', description: 'Filter lists by level count' }, + hasChildren: { type: 'boolean', description: 'Filter list items that have children' }, + shortCode: { type: 'string', description: 'Filter list items by short code' }, + shortCodeOrValue: { type: 'string', description: 'Filter list items by short code or value' }, + budgetId: { type: 'string', description: 'Budget header ID' }, + adminView: { type: 'boolean', description: 'Return all admin-visible budgets' }, + responseSchema: { type: 'string', description: 'Budget response schema (COMPACT)' }, + purchaseRequestId: { type: 'string', description: 'Purchase request ID' }, + limit: { type: 'number', description: 'Max records per page' }, + offset: { type: 'number', description: 'Page offset' }, + start: { type: 'number', description: 'Page start cursor (offset)' }, + body: { type: 'json', description: 'JSON request body' }, + useridType: { type: 'string', description: 'Travel profile identifier type' }, + useridValue: { type: 'string', description: 'Travel profile identifier value' }, + lastModifiedDate: { type: 'string', description: 'Required ISO date for profile summary' }, + page: { type: 'number', description: 'Page number (lists/list_items)' }, + travelProfilePage: { type: 'number', description: 'Profile summary page number' }, + itemsPerPage: { type: 'number', description: 'Profile summary items per page' }, + travelProfileActive: { type: 'string', description: 'Status filter ("Active" or "Inactive")' }, + travelConfigs: { type: 'string', description: 'Comma-separated travel config ids' }, + searchText: { type: 'string', description: 'Locations v5 free-text search' }, + locCode: { type: 'string', description: 'Locations v5 location code' }, + locationNameId: { type: 'string', description: 'Locations v5 location name id' }, + locationNameKey: { type: 'number', description: 'Locations v5 numeric location name key' }, + countryCode: { type: 'string', description: 'Locations v5 ISO 3166-1 country code' }, + subdivisionCode: { type: 'string', description: 'Locations v5 ISO 3166-2 subdivision code' }, + adminRegionId: { type: 'string', description: 'Locations v5 administrative region id' }, + receipt: { type: 'json', description: 'Receipt image file (canonical param)' }, + forwardId: { type: 'string', description: 'Optional dedup id for receipt upload' }, + reportsToApproveSort: { + type: 'string', + description: 'Sort field for reportsToApprove (e.g., reportDate)', + }, + reportsToApproveOrder: { type: 'string', description: 'Sort order: asc or desc' }, + includeDelegateApprovals: { + type: 'boolean', + description: 'Include reports the caller can approve as a delegate', + }, + includeAllComments: { + type: 'boolean', + description: 'Include comments from all expenses in the report', + }, + }, + outputs: { + success: { type: 'boolean', description: 'Whether the operation succeeded' }, + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { type: 'json', description: 'Concur API response payload' }, + }, +} diff --git a/apps/sim/blocks/blocks/sap_s4hana.ts b/apps/sim/blocks/blocks/sap_s4hana.ts index 30ff9b900b4..b5b985b461b 100644 --- a/apps/sim/blocks/blocks/sap_s4hana.ts +++ b/apps/sim/blocks/blocks/sap_s4hana.ts @@ -71,6 +71,25 @@ export const SapS4HanaBlock: BlockConfig = { title: '$filter', type: 'long-input', placeholder: "BusinessPartnerCategory eq '1'", + wandConfig: { + enabled: true, + prompt: `Generate an OData v2 $filter expression for SAP S/4HANA based on the user's request. + +Rules: +- String literals are single-quoted, e.g. eq '1010' +- Combine clauses with 'and' / 'or' +- Common operators: eq, ne, gt, ge, lt, le +- Date/time literals use datetime'YYYY-MM-DDTHH:MM:SS' +- Functions: substringof('x', Field), startswith(Field, 'x'), endswith(Field, 'x') + +Examples: +- BusinessPartnerCategory eq '1' and Country eq 'US' +- CreationDate gt datetime'2024-01-01T00:00:00' +- substringof('ACME', OrganizationBPName1) + +Return ONLY the $filter expression - no explanations, no extra text.`, + placeholder: 'Describe the filter you want (e.g., "people in the US created this year")', + }, condition: { field: 'operation', value: [ @@ -405,6 +424,22 @@ export const SapS4HanaBlock: BlockConfig = { placeholder: '[{"Material":"TG11","RequestedQuantity":"1"}]', condition: { field: 'operation', value: 'sap_s4hana_create_sales_order' }, required: true, + wandConfig: { + enabled: true, + prompt: `Generate a JSON array of SAP S/4HANA A_SalesOrderItem objects for a deep-insert under to_Item. + +Rules: +- Output a JSON array, each element an item object +- Common fields: Material (string), RequestedQuantity (string-decimal), RequestedQuantityUnit (e.g., "PC"), Plant (4-char), SalesOrderItemCategory +- Numbers in OData v2 decimals are passed as strings (e.g., "5", "10.5") + +Examples: +- [{"Material":"TG11","RequestedQuantity":"1"}] +- [{"Material":"MZ-FG-M100","RequestedQuantity":"5","RequestedQuantityUnit":"PC","Plant":"1010"}] + +Return ONLY the JSON array - no explanations, no extra text.`, + placeholder: 'Describe the items (e.g., "5 units of material TG11 from plant 1010")', + }, }, { id: 'salesOrderBody', @@ -456,7 +491,7 @@ export const SapS4HanaBlock: BlockConfig = { id: 'purchaseRequisition', title: 'PurchaseRequisition', type: 'short-input', - placeholder: '10000000', + placeholder: '0010000000', condition: { field: 'operation', value: ['sap_s4hana_get_purchase_requisition', 'sap_s4hana_update_purchase_requisition'], @@ -485,7 +520,7 @@ export const SapS4HanaBlock: BlockConfig = { id: 'purchaseRequisitionBody', title: 'Additional Fields (JSON)', type: 'code', - placeholder: '{"PurchaseRequisitionDescription":"Office supplies"}', + placeholder: '{"PurReqnDescription":"Office supplies"}', condition: { field: 'operation', value: 'sap_s4hana_create_purchase_requisition' }, mode: 'advanced', }, diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 54b2312c086..f5d9b40fd3d 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -171,6 +171,7 @@ import { RouterBlock, RouterV2Block } from '@/blocks/blocks/router' import { RssBlock } from '@/blocks/blocks/rss' import { S3Block } from '@/blocks/blocks/s3' import { SalesforceBlock } from '@/blocks/blocks/salesforce' +import { SapConcurBlock } from '@/blocks/blocks/sap_concur' import { SapS4HanaBlock } from '@/blocks/blocks/sap_s4hana' import { ScheduleBlock } from '@/blocks/blocks/schedule' import { SearchBlock } from '@/blocks/blocks/search' @@ -424,6 +425,7 @@ export const registry: Record = { rss: RssBlock, s3: S3Block, salesforce: SalesforceBlock, + sap_concur: SapConcurBlock, sap_s4hana: SapS4HanaBlock, schedule: ScheduleBlock, search: SearchBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index c4bc260742b..4092f8c10a7 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -4141,6 +4141,25 @@ export function SapS4HanaIcon(props: SVGProps) { ) } +export function SapConcurIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function ServiceNowIcon(props: SVGProps) { return ( diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index e56fb11b9e2..71601e636e3 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2270,6 +2270,78 @@ import { salesforceUpdateOpportunityTool, salesforceUpdateTaskTool, } from '@/tools/salesforce' +import { + approveExpenseReportTool as sapConcurApproveExpenseReportTool, + associateAttendeesTool as sapConcurAssociateAttendeesTool, + createCashAdvanceTool as sapConcurCreateCashAdvanceTool, + createExpectedExpenseTool as sapConcurCreateExpectedExpenseTool, + createExpenseReportTool as sapConcurCreateExpenseReportTool, + createListItemTool as sapConcurCreateListItemTool, + createPurchaseRequestTool as sapConcurCreatePurchaseRequestTool, + createQuickExpenseTool as sapConcurCreateQuickExpenseTool, + createQuickExpenseWithImageTool as sapConcurCreateQuickExpenseWithImageTool, + createReportCommentTool as sapConcurCreateReportCommentTool, + createTravelRequestTool as sapConcurCreateTravelRequestTool, + createUserTool as sapConcurCreateUserTool, + deleteExpectedExpenseTool as sapConcurDeleteExpectedExpenseTool, + deleteExpenseReportTool as sapConcurDeleteExpenseReportTool, + deleteExpenseTool as sapConcurDeleteExpenseTool, + deleteListItemTool as sapConcurDeleteListItemTool, + deleteTravelRequestTool as sapConcurDeleteTravelRequestTool, + deleteUserTool as sapConcurDeleteUserTool, + getAllocationTool as sapConcurGetAllocationTool, + getBudgetTool as sapConcurGetBudgetTool, + getCashAdvanceTool as sapConcurGetCashAdvanceTool, + getExchangeRateTool as sapConcurGetExchangeRateTool, + getExpectedExpenseTool as sapConcurGetExpectedExpenseTool, + getExpenseReportTool as sapConcurGetExpenseReportTool, + getExpenseTool as sapConcurGetExpenseTool, + getItemizationsTool as sapConcurGetItemizationsTool, + getItineraryTool as sapConcurGetItineraryTool, + getListItemTool as sapConcurGetListItemTool, + getListTool as sapConcurGetListTool, + getPurchaseRequestTool as sapConcurGetPurchaseRequestTool, + getReceiptStatusTool as sapConcurGetReceiptStatusTool, + getReceiptTool as sapConcurGetReceiptTool, + getRequestCashAdvanceTool as sapConcurGetRequestCashAdvanceTool, + getTravelProfileTool as sapConcurGetTravelProfileTool, + getTravelRequestTool as sapConcurGetTravelRequestTool, + getUserTool as sapConcurGetUserTool, + issueCashAdvanceTool as sapConcurIssueCashAdvanceTool, + listAllocationsTool as sapConcurListAllocationsTool, + listAttendeeAssociationsTool as sapConcurListAttendeeAssociationsTool, + listBudgetCategoriesTool as sapConcurListBudgetCategoriesTool, + listBudgetsTool as sapConcurListBudgetsTool, + listExceptionsTool as sapConcurListExceptionsTool, + listExpectedExpensesTool as sapConcurListExpectedExpensesTool, + listExpenseReportsTool as sapConcurListExpenseReportsTool, + listExpensesTool as sapConcurListExpensesTool, + listItinerariesTool as sapConcurListItinerariesTool, + listListItemsTool as sapConcurListListItemsTool, + listListsTool as sapConcurListListsTool, + listReceiptsTool as sapConcurListReceiptsTool, + listReportCommentsTool as sapConcurListReportCommentsTool, + listReportsToApproveTool as sapConcurListReportsToApproveTool, + listTravelProfilesSummaryTool as sapConcurListTravelProfilesSummaryTool, + listTravelRequestCommentsTool as sapConcurListTravelRequestCommentsTool, + listTravelRequestsTool as sapConcurListTravelRequestsTool, + listUsersTool as sapConcurListUsersTool, + moveTravelRequestTool as sapConcurMoveTravelRequestTool, + recallExpenseReportTool as sapConcurRecallExpenseReportTool, + removeAllAttendeesTool as sapConcurRemoveAllAttendeesTool, + searchLocationsTool as sapConcurSearchLocationsTool, + searchUsersTool as sapConcurSearchUsersTool, + sendBackExpenseReportTool as sapConcurSendBackExpenseReportTool, + submitExpenseReportTool as sapConcurSubmitExpenseReportTool, + updateAllocationTool as sapConcurUpdateAllocationTool, + updateExpectedExpenseTool as sapConcurUpdateExpectedExpenseTool, + updateExpenseReportTool as sapConcurUpdateExpenseReportTool, + updateExpenseTool as sapConcurUpdateExpenseTool, + updateListItemTool as sapConcurUpdateListItemTool, + updateTravelRequestTool as sapConcurUpdateTravelRequestTool, + updateUserTool as sapConcurUpdateUserTool, + uploadReceiptImageTool as sapConcurUploadReceiptImageTool, +} from '@/tools/sap_concur' import { createBusinessPartnerTool as sapS4HanaCreateBusinessPartnerTool, createPurchaseOrderTool as sapS4HanaCreatePurchaseOrderTool, @@ -5370,6 +5442,76 @@ export const tools: Record = { salesforce_query_more: salesforceQueryMoreTool, salesforce_describe_object: salesforceDescribeObjectTool, salesforce_list_objects: salesforceListObjectsTool, + sap_concur_approve_expense_report: sapConcurApproveExpenseReportTool, + sap_concur_associate_attendees: sapConcurAssociateAttendeesTool, + sap_concur_create_cash_advance: sapConcurCreateCashAdvanceTool, + sap_concur_create_expected_expense: sapConcurCreateExpectedExpenseTool, + sap_concur_create_expense_report: sapConcurCreateExpenseReportTool, + sap_concur_create_list_item: sapConcurCreateListItemTool, + sap_concur_create_purchase_request: sapConcurCreatePurchaseRequestTool, + sap_concur_create_quick_expense: sapConcurCreateQuickExpenseTool, + sap_concur_create_quick_expense_with_image: sapConcurCreateQuickExpenseWithImageTool, + sap_concur_create_report_comment: sapConcurCreateReportCommentTool, + sap_concur_create_travel_request: sapConcurCreateTravelRequestTool, + sap_concur_create_user: sapConcurCreateUserTool, + sap_concur_delete_expected_expense: sapConcurDeleteExpectedExpenseTool, + sap_concur_delete_expense: sapConcurDeleteExpenseTool, + sap_concur_delete_expense_report: sapConcurDeleteExpenseReportTool, + sap_concur_delete_list_item: sapConcurDeleteListItemTool, + sap_concur_delete_travel_request: sapConcurDeleteTravelRequestTool, + sap_concur_delete_user: sapConcurDeleteUserTool, + sap_concur_get_allocation: sapConcurGetAllocationTool, + sap_concur_get_budget: sapConcurGetBudgetTool, + sap_concur_get_cash_advance: sapConcurGetCashAdvanceTool, + sap_concur_get_exchange_rate: sapConcurGetExchangeRateTool, + sap_concur_get_expected_expense: sapConcurGetExpectedExpenseTool, + sap_concur_get_expense: sapConcurGetExpenseTool, + sap_concur_get_expense_report: sapConcurGetExpenseReportTool, + sap_concur_get_itemizations: sapConcurGetItemizationsTool, + sap_concur_get_itinerary: sapConcurGetItineraryTool, + sap_concur_get_list: sapConcurGetListTool, + sap_concur_get_list_item: sapConcurGetListItemTool, + sap_concur_get_purchase_request: sapConcurGetPurchaseRequestTool, + sap_concur_get_receipt: sapConcurGetReceiptTool, + sap_concur_get_receipt_status: sapConcurGetReceiptStatusTool, + sap_concur_get_request_cash_advance: sapConcurGetRequestCashAdvanceTool, + sap_concur_get_travel_profile: sapConcurGetTravelProfileTool, + sap_concur_get_travel_request: sapConcurGetTravelRequestTool, + sap_concur_get_user: sapConcurGetUserTool, + sap_concur_issue_cash_advance: sapConcurIssueCashAdvanceTool, + sap_concur_list_allocations: sapConcurListAllocationsTool, + sap_concur_list_attendee_associations: sapConcurListAttendeeAssociationsTool, + sap_concur_list_budget_categories: sapConcurListBudgetCategoriesTool, + sap_concur_list_budgets: sapConcurListBudgetsTool, + sap_concur_list_exceptions: sapConcurListExceptionsTool, + sap_concur_list_expected_expenses: sapConcurListExpectedExpensesTool, + sap_concur_list_expenses: sapConcurListExpensesTool, + sap_concur_list_expense_reports: sapConcurListExpenseReportsTool, + sap_concur_list_itineraries: sapConcurListItinerariesTool, + sap_concur_list_lists: sapConcurListListsTool, + sap_concur_list_list_items: sapConcurListListItemsTool, + sap_concur_list_receipts: sapConcurListReceiptsTool, + sap_concur_list_report_comments: sapConcurListReportCommentsTool, + sap_concur_list_reports_to_approve: sapConcurListReportsToApproveTool, + sap_concur_list_travel_profiles_summary: sapConcurListTravelProfilesSummaryTool, + sap_concur_list_travel_request_comments: sapConcurListTravelRequestCommentsTool, + sap_concur_list_travel_requests: sapConcurListTravelRequestsTool, + sap_concur_list_users: sapConcurListUsersTool, + sap_concur_move_travel_request: sapConcurMoveTravelRequestTool, + sap_concur_recall_expense_report: sapConcurRecallExpenseReportTool, + sap_concur_remove_all_attendees: sapConcurRemoveAllAttendeesTool, + sap_concur_search_locations: sapConcurSearchLocationsTool, + sap_concur_search_users: sapConcurSearchUsersTool, + sap_concur_send_back_expense_report: sapConcurSendBackExpenseReportTool, + sap_concur_submit_expense_report: sapConcurSubmitExpenseReportTool, + sap_concur_update_allocation: sapConcurUpdateAllocationTool, + sap_concur_update_expected_expense: sapConcurUpdateExpectedExpenseTool, + sap_concur_update_expense: sapConcurUpdateExpenseTool, + sap_concur_update_expense_report: sapConcurUpdateExpenseReportTool, + sap_concur_update_list_item: sapConcurUpdateListItemTool, + sap_concur_update_travel_request: sapConcurUpdateTravelRequestTool, + sap_concur_update_user: sapConcurUpdateUserTool, + sap_concur_upload_receipt_image: sapConcurUploadReceiptImageTool, sap_s4hana_create_business_partner: sapS4HanaCreateBusinessPartnerTool, sap_s4hana_create_purchase_order: sapS4HanaCreatePurchaseOrderTool, sap_s4hana_create_purchase_requisition: sapS4HanaCreatePurchaseRequisitionTool, diff --git a/apps/sim/tools/sap_concur/approve_expense_report.ts b/apps/sim/tools/sap_concur/approve_expense_report.ts new file mode 100644 index 00000000000..5320989be85 --- /dev/null +++ b/apps/sim/tools/sap_concur/approve_expense_report.ts @@ -0,0 +1,95 @@ +import type { ApproveExpenseReportParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const approveExpenseReportTool: ToolConfig< + ApproveExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_approve_expense_report', + name: 'SAP Concur Approve Expense Report', + description: + 'Approve an expense report as a manager (PATCH /expensereports/v4/reports/{reportId}/approve). Required body field: comment.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID to approve', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Request body — `comment` is required by Concur (e.g., { "comment": "Approved" }). If the report contains rejected expenses, `expenseRejectedComment` is also required. Optional fields: `expectedStepCode`, `expectedStepSequence`, `statusId` (defaults to "A_APPR").', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const reportId = trimRequired(params.reportId, 'reportId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/reports/${encodeURIComponent(reportId)}/approve`, + method: 'PATCH', + body: params.body, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { type: 'json', description: 'Empty (204 No Content)' }, + }, +} diff --git a/apps/sim/tools/sap_concur/associate_attendees.ts b/apps/sim/tools/sap_concur/associate_attendees.ts new file mode 100644 index 00000000000..c452dce4e09 --- /dev/null +++ b/apps/sim/tools/sap_concur/associate_attendees.ts @@ -0,0 +1,123 @@ +import type { AssociateAttendeesParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const associateAttendeesTool: ToolConfig = + { + id: 'sap_concur_associate_attendees', + name: 'SAP Concur Associate Attendees', + description: + 'Associate attendees with an expense (POST /expensereports/v4/users/{userId}/context/TRAVELER/reports/{reportId}/expenses/{expenseId}/attendees).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: TRAVELER or PROXY', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + expenseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense ID', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Attendee associations payload (e.g., { "attendeeAssociations": [...] })', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + const expenseId = trimRequired(params.expenseId, 'expenseId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/expenses/${encodeURIComponent(expenseId)}/attendees`, + method: 'POST', + body: params.body, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Concur association response (201 Created with URI)', + properties: { + uri: { + type: 'string', + description: 'Resource URI of the attendee associations collection', + optional: true, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/create_cash_advance.ts b/apps/sim/tools/sap_concur/create_cash_advance.ts new file mode 100644 index 00000000000..e58fd3ac707 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_cash_advance.ts @@ -0,0 +1,90 @@ +import type { CreateCashAdvanceParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createCashAdvanceTool: ToolConfig = { + id: 'sap_concur_create_cash_advance', + name: 'SAP Concur Create Cash Advance', + description: 'Create a cash advance (POST /cashadvance/v4/cashadvances).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Cash advance payload', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + ...baseProxyBody(params), + path: `/cashadvance/v4/cashadvances`, + method: 'POST', + body: params.body, + }), + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Created cash advance payload', + properties: { + cashAdvanceId: { + type: 'string', + description: 'Unique identifier of the created cash advance', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_expected_expense.ts b/apps/sim/tools/sap_concur/create_expected_expense.ts new file mode 100644 index 00000000000..e9bf810abac --- /dev/null +++ b/apps/sim/tools/sap_concur/create_expected_expense.ts @@ -0,0 +1,179 @@ +import type { CreateExpectedExpenseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createExpectedExpenseTool: ToolConfig< + CreateExpectedExpenseParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_create_expected_expense', + name: 'SAP Concur Create Expected Expense', + description: + 'Create an expected expense on a travel request (POST /travelrequest/v4/requests/{requestUuid}/expenses).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + requestUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Travel request UUID', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'User UUID acting on the request (required when using a Company JWT, optional otherwise)', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Expected expense payload', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const requestUuid = trimRequired(params.requestUuid, 'requestUuid') + const query: Record = {} + if (params.userId?.trim()) query.userId = params.userId.trim() + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests/${encodeURIComponent(requestUuid)}/expenses`, + method: 'POST', + body: params.body, + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Created expected expense payload', + properties: { + id: { type: 'string', description: 'Expected expense identifier', optional: true }, + href: { type: 'string', description: 'Self-link to the resource', optional: true }, + expenseType: { + type: 'json', + description: 'Expense type {id, name}', + optional: true, + }, + transactionDate: { + type: 'string', + description: 'Transaction date', + optional: true, + }, + transactionAmount: { + type: 'json', + description: 'Transaction amount {value, currencyCode}', + optional: true, + }, + postedAmount: { + type: 'json', + description: 'Posted amount {value, currencyCode}', + optional: true, + }, + approvedAmount: { + type: 'json', + description: 'Approved amount {value, currencyCode}', + optional: true, + }, + remainingAmount: { + type: 'json', + description: 'Remaining amount on the expected expense', + optional: true, + }, + businessPurpose: { + type: 'string', + description: 'Business purpose of the expense', + optional: true, + }, + location: { + type: 'json', + description: + 'Location {id, name, city, countryCode, countrySubDivisionCode, iataCode, locationType}', + optional: true, + }, + exchangeRate: { + type: 'json', + description: 'Exchange rate {value, operation}', + optional: true, + }, + allocations: { + type: 'json', + description: + 'Budget allocations array (allocationId, allocationAmount, approvedAmount, postedAmount, expenseId, percentEdited, systemAllocation, percentage)', + optional: true, + }, + tripData: { + type: 'json', + description: + 'Trip data {agencyBooked, selfBooked, tripType (ONE_WAY|ROUND_TRIP), legs[{id, returnLeg, startDate, startTime, startLocationDetail, startLocation, endLocation, class {code,value}, travelExceptionReasonCodes}], segmentType {category, code}}', + optional: true, + }, + parentRequest: { + type: 'json', + description: 'Parent travel request resource link {href, id}', + optional: true, + }, + comments: { + type: 'json', + description: 'Comments sub-resource link {href, id}', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_expense_report.ts b/apps/sim/tools/sap_concur/create_expense_report.ts new file mode 100644 index 00000000000..2a662e55d31 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_expense_report.ts @@ -0,0 +1,109 @@ +import type { CreateExpenseReportParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createExpenseReportTool: ToolConfig< + CreateExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_create_expense_report', + name: 'SAP Concur Create Expense Report', + description: + 'Create an expense report (POST /expensereports/v4/users/{userId}/context/{contextType}/reports — supported contexts: TRAVELER, PROXY). Required body fields: name, policyId.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID who will own the report', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Access context: TRAVELER (creating own report) or PROXY (creating on behalf of another user)', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Report payload — `name` and `policyId` are required. Optional fields: businessPurpose, comment, customData, countryCode, countrySubDivisionCode, etc.', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports`, + method: 'POST', + body: params.body, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Created expense report (Concur returns 201 with a URI to the new report)', + properties: { + uri: { type: 'string', description: 'URI of the newly created expense report' }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_list_item.ts b/apps/sim/tools/sap_concur/create_list_item.ts new file mode 100644 index 00000000000..18cb420ab16 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_list_item.ts @@ -0,0 +1,121 @@ +import type { CreateListItemParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createListItemTool: ToolConfig = { + id: 'sap_concur_create_list_item', + name: 'SAP Concur Create List Item', + description: 'Create a list item (POST /list/v4/items).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'List item payload. Required: listId, shortCode, value. Optional: parentId or parentCode (mutually exclusive). Note: Concur rejects shortCode/value containing hyphens.', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + ...baseProxyBody(params), + path: '/list/v4/items', + method: 'POST', + body: params.body, + }), + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Created list item', + properties: { + id: { type: 'string', description: 'List item UUID', optional: true }, + code: { type: 'string', description: 'Long code format for the item', optional: true }, + shortCode: { type: 'string', description: 'Short code identifier', optional: true }, + value: { type: 'string', description: 'Display value of the item', optional: true }, + parentId: { + type: 'string', + description: 'Parent item UUID (omitted for first-level items)', + optional: true, + }, + level: { + type: 'number', + description: 'Hierarchy level (1 for root items)', + optional: true, + }, + isDeleted: { + type: 'boolean', + description: 'Deletion status across all containing lists', + optional: true, + }, + lists: { + type: 'array', + description: 'Lists containing this item', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'List UUID', optional: true }, + hasChildren: { + type: 'boolean', + description: 'Whether this item has children in the list', + optional: true, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_purchase_request.ts b/apps/sim/tools/sap_concur/create_purchase_request.ts new file mode 100644 index 00000000000..9d494c8f581 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_purchase_request.ts @@ -0,0 +1,115 @@ +import type { CreatePurchaseRequestParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createPurchaseRequestTool: ToolConfig< + CreatePurchaseRequestParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_create_purchase_request', + name: 'SAP Concur Create Purchase Request', + description: 'Create a purchase request (POST /purchaserequest/v4/purchaserequests).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Purchase request payload', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + ...baseProxyBody(params), + path: `/purchaserequest/v4/purchaserequests`, + method: 'POST', + body: params.body, + }), + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Created purchase request payload', + properties: { + id: { + type: 'string', + description: 'Identifier of the created purchase request', + optional: true, + }, + uri: { + type: 'string', + description: 'Resource URI for the created purchase request', + optional: true, + }, + errors: { + type: 'array', + description: 'Validation or processing errors returned by Concur', + optional: true, + items: { + type: 'json', + properties: { + errorCode: { type: 'string', description: 'Error code', optional: true }, + errorMessage: { type: 'string', description: 'Error message', optional: true }, + dataPath: { + type: 'string', + description: 'Path to the request data which has the error', + optional: true, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_quick_expense.ts b/apps/sim/tools/sap_concur/create_quick_expense.ts new file mode 100644 index 00000000000..8b3bfc04524 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_quick_expense.ts @@ -0,0 +1,110 @@ +import type { CreateQuickExpenseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createQuickExpenseTool: ToolConfig = + { + id: 'sap_concur_create_quick_expense', + name: 'SAP Concur Create Quick Expense', + description: + 'Create a quick expense (POST /quickexpense/v4/users/{userId}/context/TRAVELER/quickexpenses).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID who owns the quick expense', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: must be TRAVELER', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Quick expense payload (expenseTypeId, transactionAmount, transactionDate, etc.)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + return { + ...baseProxyBody(params), + path: `/quickexpense/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/quickexpenses`, + method: 'POST', + body: params.body, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Created quick expense response (HTTP 201 Created)', + properties: { + quickExpenseIdUri: { + type: 'string', + description: 'URI of the created quick expense resource', + optional: true, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/create_quick_expense_with_image.ts b/apps/sim/tools/sap_concur/create_quick_expense_with_image.ts new file mode 100644 index 00000000000..65e89569bc1 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_quick_expense_with_image.ts @@ -0,0 +1,123 @@ +import type { + CreateQuickExpenseWithImageParams, + SapConcurProxyResponse, +} from '@/tools/sap_concur/types' +import { SAP_CONCUR_UPLOAD_URL } from '@/tools/sap_concur/upload_receipt_image' +import { + baseProxyBody, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createQuickExpenseWithImageTool: ToolConfig< + CreateQuickExpenseWithImageParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_create_quick_expense_with_image', + name: 'SAP Concur Create Quick Expense With Image', + description: + 'Create a quick expense with an attached image (POST /quickexpense/v4/users/{userId}/context/{contextType}/quickexpenses/image).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: must be TRAVELER', + }, + receipt: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Receipt image (UserFile). Allowed: PDF, PNG, JPEG, TIFF (max 50MB)', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Quick expense payload (transactionAmount, transactionDate, expenseTypeId, vendor, ...)', + }, + }, + request: { + url: SAP_CONCUR_UPLOAD_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + return { + ...baseProxyBody(params), + operation: 'create_quick_expense_with_image', + userId, + contextType, + receipt: params.receipt, + body: params.body, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Created quick expense response (HTTP 201 with attached receipt image)', + properties: { + quickExpenseIdUri: { + type: 'string', + description: 'URI of the created quick expense resource', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_report_comment.ts b/apps/sim/tools/sap_concur/create_report_comment.ts new file mode 100644 index 00000000000..add695d3085 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_report_comment.ts @@ -0,0 +1,119 @@ +import type { CreateReportCommentParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createReportCommentTool: ToolConfig< + CreateReportCommentParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_create_report_comment', + name: 'SAP Concur Create Report Comment', + description: + 'Create a comment on a report (POST /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/comments).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: TRAVELER, MANAGER, or PROXY', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + comment: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comment text to add', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + const comment = trimRequired(params.comment, 'comment') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/comments`, + method: 'POST', + body: { comment }, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Created comment response (Concur returns 201 Created with URI)', + properties: { + uri: { + type: 'string', + description: 'Resource URI of the created comment', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_travel_request.ts b/apps/sim/tools/sap_concur/create_travel_request.ts new file mode 100644 index 00000000000..ee82e0301b2 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_travel_request.ts @@ -0,0 +1,264 @@ +import type { CreateTravelRequestParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createTravelRequestTool: ToolConfig< + CreateTravelRequestParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_create_travel_request', + name: 'SAP Concur Create Travel Request', + description: 'Create a travel request (POST /travelrequest/v4/requests).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Concur user UUID — required when impersonating another user', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Travel request payload (name, purpose, startDate, endDate, requestPolicyId, etc.)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const query: Record = {} + if (params.userId) query.userId = params.userId + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests`, + method: 'POST', + body: params.body, + query: Object.keys(query).length > 0 ? query : undefined, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Created travel request payload', + properties: { + id: { type: 'string', description: 'Travel request UUID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + requestId: { + type: 'string', + description: 'Public-facing request ID (4-6 alphanumeric characters)', + optional: true, + }, + name: { type: 'string', description: 'Request name', optional: true }, + businessPurpose: { type: 'string', description: 'Business purpose', optional: true }, + comment: { type: 'string', description: 'Last attached comment', optional: true }, + creationDate: { type: 'string', description: 'Creation timestamp', optional: true }, + lastModified: { + type: 'string', + description: 'Last modification timestamp', + optional: true, + }, + submitDate: { type: 'string', description: 'Last submission timestamp', optional: true }, + startDate: { type: 'string', description: 'Trip start date (ISO 8601)', optional: true }, + endDate: { type: 'string', description: 'Trip end date (ISO 8601)', optional: true }, + startTime: { type: 'string', description: 'Trip start time (HH:mm)', optional: true }, + endTime: { type: 'string', description: 'Trip end time (HH:mm)', optional: true }, + approved: { + type: 'boolean', + description: 'Whether the request is approved', + optional: true, + }, + pendingApproval: { type: 'boolean', description: 'Pending approval flag', optional: true }, + closed: { type: 'boolean', description: 'Closed flag', optional: true }, + everSentBack: { type: 'boolean', description: 'Ever-sent-back flag', optional: true }, + canceledPostApproval: { + type: 'boolean', + description: 'Canceled after approval flag', + optional: true, + }, + approvalStatus: { + type: 'json', + description: 'Approval status', + optional: true, + properties: { + code: { + type: 'string', + description: 'Status code (NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK)', + optional: true, + }, + name: { type: 'string', description: 'Localized status name', optional: true }, + }, + }, + owner: { + type: 'json', + description: 'Travel request owner', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { type: 'string', description: 'Owner first name', optional: true }, + lastName: { type: 'string', description: 'Owner last name', optional: true }, + }, + }, + approver: { + type: 'json', + description: 'Approver assigned to the request', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { type: 'string', description: 'Approver first name', optional: true }, + lastName: { type: 'string', description: 'Approver last name', optional: true }, + }, + }, + policy: { + type: 'json', + description: 'Resource link to the applicable policy', + optional: true, + properties: { + id: { type: 'string', description: 'Policy ID', optional: true }, + href: { type: 'string', description: 'Policy hyperlink', optional: true }, + }, + }, + type: { + type: 'json', + description: 'Request type', + optional: true, + properties: { + code: { type: 'string', description: 'Request type code', optional: true }, + label: { type: 'string', description: 'Request type label', optional: true }, + }, + }, + mainDestination: { + type: 'json', + description: 'Main destination of the trip', + optional: true, + properties: { + city: { type: 'string', description: 'City', optional: true }, + countryCode: { type: 'string', description: 'ISO country code', optional: true }, + countrySubDivisionCode: { + type: 'string', + description: 'ISO country sub-division code', + optional: true, + }, + name: { type: 'string', description: 'Destination name', optional: true }, + }, + }, + totalApprovedAmount: { + type: 'json', + description: 'Total approved amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + totalPostedAmount: { + type: 'json', + description: 'Total posted amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + totalRemainingAmount: { + type: 'json', + description: 'Total remaining amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + operations: { + type: 'array', + description: 'Available workflow actions', + optional: true, + items: { + type: 'json', + properties: { + rel: { type: 'string', description: 'Operation name', optional: true }, + href: { type: 'string', description: 'Operation URL', optional: true }, + }, + }, + }, + expenses: { + type: 'array', + description: 'Expected expenses attached to the request', + optional: true, + items: { type: 'json' }, + }, + highestExceptionLevel: { + type: 'string', + description: 'Highest exception level (NONE, WARNING, ERROR)', + optional: true, + }, + travelAgency: { + type: 'json', + description: 'Travel agency reference', + optional: true, + properties: { + id: { type: 'string', description: 'Agency identifier', optional: true }, + href: { type: 'string', description: 'Agency URL', optional: true }, + template: { type: 'string', description: 'Template URL', optional: true }, + }, + }, + custom1: { type: 'json', description: 'Custom field 1', optional: true }, + custom2: { type: 'json', description: 'Custom field 2', optional: true }, + custom3: { type: 'json', description: 'Custom field 3', optional: true }, + custom4: { type: 'json', description: 'Custom field 4', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_user.ts b/apps/sim/tools/sap_concur/create_user.ts new file mode 100644 index 00000000000..5460052e4d7 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_user.ts @@ -0,0 +1,85 @@ +import type { CreateUserParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + scimUserOutputProperties, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createUserTool: ToolConfig = { + id: 'sap_concur_create_user', + name: 'SAP Concur Create User', + description: 'Create a new user identity (POST /profile/identity/v4.1/Users).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'SCIM User payload (schemas, userName, name, emails, active, etc.)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + ...baseProxyBody(params), + path: `/profile/identity/v4.1/Users`, + method: 'POST', + body: params.body, + }), + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Created SCIM User payload', + properties: scimUserOutputProperties, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/delete_expected_expense.ts b/apps/sim/tools/sap_concur/delete_expected_expense.ts new file mode 100644 index 00000000000..aa2bc2b5c28 --- /dev/null +++ b/apps/sim/tools/sap_concur/delete_expected_expense.ts @@ -0,0 +1,100 @@ +import type { DeleteExpectedExpenseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteExpectedExpenseTool: ToolConfig< + DeleteExpectedExpenseParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_delete_expected_expense', + name: 'SAP Concur Delete Expected Expense', + description: 'Delete an expected expense (DELETE /travelrequest/v4/expenses/{expenseUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + expenseUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expected expense UUID to delete', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'User UUID acting on the request (required when using a Company JWT, optional otherwise)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const expenseUuid = trimRequired(params.expenseUuid, 'expenseUuid') + const query: Record = {} + if (params.userId?.trim()) query.userId = params.userId.trim() + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/expenses/${encodeURIComponent(expenseUuid)}`, + method: 'DELETE', + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Returns boolean true on 200 OK when the expected expense is deleted.', + properties: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/delete_expense.ts b/apps/sim/tools/sap_concur/delete_expense.ts new file mode 100644 index 00000000000..15122af939b --- /dev/null +++ b/apps/sim/tools/sap_concur/delete_expense.ts @@ -0,0 +1,96 @@ +import type { DeleteExpenseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteExpenseTool: ToolConfig = { + id: 'sap_concur_delete_expense', + name: 'SAP Concur Delete Expense', + description: + 'Delete an expense (DELETE /expensereports/v4/reports/{reportId}/expenses/{expenseId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + expenseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense ID to delete', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const reportId = trimRequired(params.reportId, 'reportId') + const expenseId = trimRequired(params.expenseId, 'expenseId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/reports/${encodeURIComponent(reportId)}/expenses/${encodeURIComponent(expenseId)}`, + method: 'DELETE', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: + 'Empty body on success (HTTP 204 No Content). Error details when status is non-2xx', + properties: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/delete_expense_report.ts b/apps/sim/tools/sap_concur/delete_expense_report.ts new file mode 100644 index 00000000000..95d8bd6c621 --- /dev/null +++ b/apps/sim/tools/sap_concur/delete_expense_report.ts @@ -0,0 +1,86 @@ +import type { DeleteExpenseReportParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteExpenseReportTool: ToolConfig< + DeleteExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_delete_expense_report', + name: 'SAP Concur Delete Expense Report', + description: 'Delete an expense report (DELETE /expensereports/v4/reports/{reportId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID to delete', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const reportId = trimRequired(params.reportId, 'reportId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/reports/${encodeURIComponent(reportId)}`, + method: 'DELETE', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { type: 'json', description: 'Empty (204 No Content)' }, + }, +} diff --git a/apps/sim/tools/sap_concur/delete_list_item.ts b/apps/sim/tools/sap_concur/delete_list_item.ts new file mode 100644 index 00000000000..be3944ef372 --- /dev/null +++ b/apps/sim/tools/sap_concur/delete_list_item.ts @@ -0,0 +1,88 @@ +import type { DeleteListItemParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteListItemTool: ToolConfig = { + id: 'sap_concur_delete_list_item', + name: 'SAP Concur Delete List Item', + description: 'Delete a list item (DELETE /list/v4/items/{itemId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'List item UUID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const itemId = trimRequired(params.itemId, 'itemId') + return { + ...baseProxyBody(params), + path: `/list/v4/items/${encodeURIComponent(itemId)}`, + method: 'DELETE', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: + 'Empty body on success (HTTP 204 No Content). Error details when status is non-2xx', + properties: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/delete_travel_request.ts b/apps/sim/tools/sap_concur/delete_travel_request.ts new file mode 100644 index 00000000000..6b0a607982c --- /dev/null +++ b/apps/sim/tools/sap_concur/delete_travel_request.ts @@ -0,0 +1,99 @@ +import type { DeleteTravelRequestParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteTravelRequestTool: ToolConfig< + DeleteTravelRequestParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_delete_travel_request', + name: 'SAP Concur Delete Travel Request', + description: 'Delete a travel request (DELETE /travelrequest/v4/requests/{requestUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + requestUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Travel request UUID to delete', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Concur user UUID — required when impersonating another user', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const requestUuid = trimRequired(params.requestUuid, 'requestUuid') + const query: Record = {} + if (params.userId) query.userId = params.userId + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests/${encodeURIComponent(requestUuid)}`, + method: 'DELETE', + query: Object.keys(query).length > 0 ? query : undefined, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Concur delete response payload (boolean true on 200 OK)', + properties: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/delete_user.ts b/apps/sim/tools/sap_concur/delete_user.ts new file mode 100644 index 00000000000..8e21733eb80 --- /dev/null +++ b/apps/sim/tools/sap_concur/delete_user.ts @@ -0,0 +1,87 @@ +import type { DeleteUserParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteUserTool: ToolConfig = { + id: 'sap_concur_delete_user', + name: 'SAP Concur Delete User', + description: 'Delete a user identity (DELETE /profile/identity/v4.1/Users/{id}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User UUID to delete', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userUuid = trimRequired(params.userUuid, 'userUuid') + return { + ...baseProxyBody(params), + path: `/profile/identity/v4.1/Users/${encodeURIComponent(userUuid)}`, + method: 'DELETE', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Deletion response — empty body on HTTP 204 No Content', + properties: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_allocation.ts b/apps/sim/tools/sap_concur/get_allocation.ts new file mode 100644 index 00000000000..c164a2ece81 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_allocation.ts @@ -0,0 +1,156 @@ +import type { GetAllocationParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getAllocationTool: ToolConfig = { + id: 'sap_concur_get_allocation', + name: 'SAP Concur Get Allocation', + description: + 'Get a single allocation (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/allocations/{allocationId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: TRAVELER or PROXY', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + allocationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Allocation ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + const allocationId = trimRequired(params.allocationId, 'allocationId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/allocations/${encodeURIComponent(allocationId)}`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Allocation detail payload', + properties: { + allocationId: { type: 'string', description: 'Unique allocation identifier' }, + accountCode: { type: 'string', optional: true, description: 'Ledger account code' }, + overLimitAccountCode: { + type: 'string', + optional: true, + description: 'Account code applied to amounts over the per-allocation limit', + }, + percentage: { type: 'number', description: 'Allocation percentage' }, + allocationAmount: { + type: 'json', + description: 'Allocation amount (value, currencyCode)', + properties: { + value: { type: 'number', description: 'Amount value' }, + currencyCode: { type: 'string', description: 'ISO 4217 currency code' }, + }, + }, + approvedAmount: { + type: 'json', + description: 'Pro-rated approved amount (value, currencyCode)', + properties: { + value: { type: 'number', description: 'Amount value' }, + currencyCode: { type: 'string', description: 'ISO 4217 currency code' }, + }, + }, + claimedAmount: { + type: 'json', + description: 'Requested reimbursement amount (value, currencyCode)', + properties: { + value: { type: 'number', description: 'Amount value' }, + currencyCode: { type: 'string', description: 'ISO 4217 currency code' }, + }, + }, + customData: { + type: 'array', + optional: true, + description: 'Custom field values (id, value, isValid)', + }, + expenseId: { type: 'string', description: 'Associated expense identifier' }, + isSystemAllocation: { + type: 'boolean', + description: 'True when system-managed', + }, + isPercentEdited: { + type: 'boolean', + description: 'True when the percentage was manually edited', + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_budget.ts b/apps/sim/tools/sap_concur/get_budget.ts new file mode 100644 index 00000000000..68a51d55ac8 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_budget.ts @@ -0,0 +1,179 @@ +import type { GetBudgetParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getBudgetTool: ToolConfig = { + id: 'sap_concur_get_budget', + name: 'SAP Concur Get Budget', + description: 'Get a budget item header by ID (GET /budget/v4/budgetItemHeader/{id}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + budgetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Budget item header ID (syncguid)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const budgetId = trimRequired(params.budgetId, 'budgetId') + return { + ...baseProxyBody(params), + path: `/budget/v4/budgetItemHeader/${encodeURIComponent(budgetId)}`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Budget header detail payload', + properties: { + id: { type: 'string', description: 'Budget item header ID' }, + name: { type: 'string', description: 'Admin-facing budget name' }, + description: { type: 'string', description: 'User-friendly display name' }, + budgetItemStatusType: { + type: 'string', + description: 'Status: OPEN, CLOSED, or REMOVED', + }, + budgetType: { + type: 'string', + optional: true, + description: 'Type: PERSONAL_USE, BUDGET, RESTRICTED, or TEAM', + }, + periodType: { + type: 'string', + optional: true, + description: 'Period type: YEARLY, QUARTERLY, MONTHLY, or DATE_RANGE', + }, + currencyCode: { type: 'string', optional: true, description: 'ISO 4217 currency code' }, + isTest: { type: 'boolean', optional: true, description: 'Test budget flag' }, + active: { type: 'boolean', optional: true, description: 'Display availability flag' }, + owned: { type: 'boolean', optional: true, description: 'Caller ownership flag' }, + annualBudget: { type: 'number', optional: true, description: 'Total annual budget amount' }, + createdDate: { type: 'string', optional: true, description: 'UTC creation timestamp' }, + lastModifiedDate: { + type: 'string', + optional: true, + description: 'UTC modification timestamp', + }, + fiscalYear: { + type: 'json', + optional: true, + description: 'Fiscal year reference (id, name, startDate, endDate, status)', + }, + budgetAmounts: { + type: 'json', + optional: true, + description: + 'Aggregate spend amounts (pendingAmount, spendAmount, unExpensedAmount, availableAmount, adjustedBudgetAmount, consumedPercent, threshold)', + }, + owner: { + type: 'json', + optional: true, + description: 'Owner user (externalUserCUUID, employeeUuid, email, employeeId, name)', + }, + budgetManagers: { + type: 'array', + optional: true, + description: 'Manager user objects', + items: { type: 'json' }, + }, + budgetApprovers: { + type: 'array', + optional: true, + description: 'Approver user objects', + items: { type: 'json' }, + }, + budgetViewers: { + type: 'array', + optional: true, + description: 'Viewer user objects', + items: { type: 'json' }, + }, + budgetTeamMembers: { + type: 'array', + optional: true, + description: 'Team member entries (budgetPerson, startDate, endDate, active, status)', + items: { type: 'json' }, + }, + budgetCategory: { + type: 'json', + optional: true, + description: 'Linked category (id, name, description, statusType)', + }, + costObjects: { + type: 'array', + optional: true, + description: 'Tracking field values (fieldDefinitionId, code, value, operator)', + items: { type: 'json' }, + }, + budgetItemDetails: { + type: 'array', + optional: true, + description: + 'Per-period detail entries (id, currencyCode, amount, budgetItemDetailStatusType, fiscalPeriod, budgetAmounts)', + items: { type: 'json' }, + }, + dateRange: { + type: 'json', + optional: true, + description: 'Date range for DATE_RANGE budgets (startDate, endDate)', + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_cash_advance.ts b/apps/sim/tools/sap_concur/get_cash_advance.ts new file mode 100644 index 00000000000..49e0e71f7b2 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_cash_advance.ts @@ -0,0 +1,191 @@ +import type { GetCashAdvanceParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getCashAdvanceTool: ToolConfig = { + id: 'sap_concur_get_cash_advance', + name: 'SAP Concur Get Cash Advance', + description: 'Get a cash advance (GET /cashadvance/v4/cashadvances/{cashadvanceId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + cashAdvanceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Cash advance ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const cashAdvanceId = trimRequired(params.cashAdvanceId, 'cashAdvanceId') + return { + ...baseProxyBody(params), + path: `/cashadvance/v4/cashadvances/${encodeURIComponent(cashAdvanceId)}`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Cash advance detail payload', + properties: { + cashAdvanceId: { type: 'string', description: 'Unique identifier of the cash advance' }, + name: { type: 'string', description: 'Cash advance name', optional: true }, + purpose: { + type: 'string', + description: 'Purpose for the cash advance', + optional: true, + }, + comment: { + type: 'string', + description: 'Comment recorded on the cash advance', + optional: true, + }, + accountCode: { + type: 'string', + description: 'Account code linked to the employee', + optional: true, + }, + requestDate: { + type: 'string', + description: 'Datetime the cash advance was requested (UTC, YYYY-MM-DD hh:mm:ss)', + optional: true, + }, + issuedDate: { + type: 'string', + description: 'Datetime the cash advance was issued (UTC, YYYY-MM-DD hh:mm:ss)', + optional: true, + }, + lastModifiedDate: { + type: 'string', + description: 'Datetime the cash advance was last modified (UTC, YYYY-MM-DD hh:mm:ss)', + optional: true, + }, + hasReceipts: { + type: 'boolean', + description: 'Whether the cash advance has receipts', + optional: true, + }, + reimbursementCurrency: { + type: 'string', + description: 'Reimbursement currency (3-letter ISO 4217 currency code)', + optional: true, + }, + amountRequested: { + type: 'json', + description: 'Amount requested for the cash advance', + optional: true, + properties: { + amount: { type: 'string', description: 'Requested amount value', optional: true }, + currency: { + type: 'string', + description: '3-letter ISO 4217 currency code', + optional: true, + }, + }, + }, + availableBalance: { + type: 'json', + description: 'Unsubmitted balance for the cash advance', + optional: true, + properties: { + amount: { type: 'string', description: 'Balance amount', optional: true }, + currency: { + type: 'string', + description: '3-letter ISO 4217 currency code', + optional: true, + }, + }, + }, + exchangeRate: { + type: 'json', + description: 'Exchange rate that applies to the cash advance', + optional: true, + properties: { + value: { type: 'string', description: 'Exchange rate value', optional: true }, + operation: { + type: 'string', + description: 'Exchange rate operation (MULTIPLY)', + optional: true, + }, + }, + }, + approvalStatus: { + type: 'json', + description: 'Approval status of the cash advance', + optional: true, + properties: { + code: { type: 'string', description: 'Status code', optional: true }, + name: { type: 'string', description: 'Status display name', optional: true }, + }, + }, + paymentType: { + type: 'json', + description: 'Payment type for the cash advance', + optional: true, + properties: { + paymentCode: { type: 'string', description: 'Payment type code', optional: true }, + description: { + type: 'string', + description: 'Payment method description', + optional: true, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_exchange_rate.ts b/apps/sim/tools/sap_concur/get_exchange_rate.ts new file mode 100644 index 00000000000..bd3aeb7b711 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_exchange_rate.ts @@ -0,0 +1,103 @@ +import type { GetExchangeRateParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getExchangeRateTool: ToolConfig = { + id: 'sap_concur_get_exchange_rate', + name: 'SAP Concur Get Exchange Rate', + description: + 'Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body: { currency_sets: [{ from_crn_code, to_crn_code, start_date: "YYYY-MM-DD", rate }] }', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Bulk upload body: { currency_sets: [{ from_crn_code, to_crn_code, start_date: "YYYY-MM-DD", rate }] } (max 100 entries)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + ...baseProxyBody(params), + path: `/exchangerate/v4/rates`, + method: 'POST', + body: params.body, + }), + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Bulk-upload exchange rate response (Exchange Rate v4)', + properties: { + overallStatus: { + type: 'string', + description: 'Overall result status for the bulk upload (e.g. SUCCESS, FAILURE)', + optional: true, + }, + message: { + type: 'string', + description: 'Top-level result message', + optional: true, + }, + currencySets: { + type: 'json', + description: + 'Per-row results: array of { from_crn_code, to_crn_code, start_date, rate, statusCode, statusMessage }', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_expected_expense.ts b/apps/sim/tools/sap_concur/get_expected_expense.ts new file mode 100644 index 00000000000..7a7507fe4da --- /dev/null +++ b/apps/sim/tools/sap_concur/get_expected_expense.ts @@ -0,0 +1,167 @@ +import type { GetExpectedExpenseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getExpectedExpenseTool: ToolConfig = + { + id: 'sap_concur_get_expected_expense', + name: 'SAP Concur Get Expected Expense', + description: 'Get an expected expense (GET /travelrequest/v4/expenses/{expenseUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + expenseUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expected expense UUID', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User UUID acting on the request (optional)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const expenseUuid = trimRequired(params.expenseUuid, 'expenseUuid') + const query: Record = {} + if (params.userId?.trim()) query.userId = params.userId.trim() + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/expenses/${encodeURIComponent(expenseUuid)}`, + method: 'GET', + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Expected expense payload', + properties: { + id: { type: 'string', description: 'Expected expense identifier', optional: true }, + href: { type: 'string', description: 'Self-link', optional: true }, + expenseType: { + type: 'json', + description: 'Expense type {id, name}', + optional: true, + }, + transactionDate: { + type: 'string', + description: 'Transaction date', + optional: true, + }, + transactionAmount: { + type: 'json', + description: 'Transaction amount {value, currencyCode}', + optional: true, + }, + postedAmount: { + type: 'json', + description: 'Posted amount {value, currencyCode}', + optional: true, + }, + approvedAmount: { + type: 'json', + description: 'Approved amount {value, currencyCode}', + optional: true, + }, + remainingAmount: { + type: 'json', + description: 'Remaining amount on the expected expense', + optional: true, + }, + businessPurpose: { + type: 'string', + description: 'Business purpose of the expense', + optional: true, + }, + location: { + type: 'json', + description: + 'Location {id, name, city, countryCode, countrySubDivisionCode, iataCode, locationType}', + optional: true, + }, + exchangeRate: { + type: 'json', + description: 'Exchange rate {value, operation}', + optional: true, + }, + allocations: { + type: 'json', + description: 'Budget allocations array', + optional: true, + }, + tripData: { + type: 'json', + description: + 'Trip data {agencyBooked, selfBooked, tripType (ONE_WAY|ROUND_TRIP), legs[{id, returnLeg, startDate, startTime, startLocationDetail, startLocation, endLocation, class {code,value}, travelExceptionReasonCodes}], segmentType {category, code}}', + optional: true, + }, + parentRequest: { + type: 'json', + description: 'Parent travel request resource link {href, id}', + optional: true, + }, + comments: { + type: 'json', + description: 'Comments sub-resource link {href, id}', + optional: true, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/get_expense.ts b/apps/sim/tools/sap_concur/get_expense.ts new file mode 100644 index 00000000000..e34dee9760a --- /dev/null +++ b/apps/sim/tools/sap_concur/get_expense.ts @@ -0,0 +1,357 @@ +import type { GetExpenseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getExpenseTool: ToolConfig = { + id: 'sap_concur_get_expense', + name: 'SAP Concur Get Expense', + description: + 'Get a single expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: TRAVELER, MANAGER, or PROXY', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + expenseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + const expenseId = trimRequired(params.expenseId, 'expenseId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/expenses/${encodeURIComponent(expenseId)}`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Expense detail (ReportExpenseDetail) payload', + properties: { + expenseId: { type: 'string', description: 'Expense identifier', optional: true }, + allocationSetId: { + type: 'string', + description: 'Identifier of the associated allocation set', + optional: true, + }, + allocationState: { + type: 'string', + description: 'FULLY_ALLOCATED, NOT_ALLOCATED, or PARTIALLY_ALLOCATED', + optional: true, + }, + expenseType: { + type: 'json', + description: 'Expense type {id, name, code, isDeleted}', + optional: true, + }, + paymentType: { + type: 'json', + description: 'Payment type {id, name, code}', + optional: true, + }, + expenseSource: { + type: 'string', + description: 'Source of the expense (CASH, CCARD, EBOOKING, etc.)', + optional: true, + }, + transactionDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + optional: true, + }, + budgetAccrualDate: { + type: 'string', + description: 'Budget accrual date', + optional: true, + }, + transactionAmount: { + type: 'json', + description: 'Transaction amount {currencyCode, value}', + optional: true, + }, + postedAmount: { + type: 'json', + description: 'Posted amount in report currency {currencyCode, value}', + optional: true, + }, + claimedAmount: { + type: 'json', + description: 'Non-personal claimed amount {currencyCode, value}', + optional: true, + }, + approvedAmount: { + type: 'json', + description: 'Approved amount {currencyCode, value}', + optional: true, + }, + approverAdjustedAmount: { + type: 'json', + description: 'Total amount adjusted by the approver', + optional: true, + }, + exchangeRate: { + type: 'json', + description: 'Exchange rate {value, operation}', + optional: true, + }, + vendor: { + type: 'json', + description: 'Vendor info {id, name, description}', + optional: true, + }, + location: { + type: 'json', + description: 'Location {id, name, city, countryCode, countrySubDivisionCode}', + optional: true, + }, + businessPurpose: { + type: 'string', + description: 'Business purpose', + optional: true, + }, + comment: { + type: 'string', + description: 'Free-form comment associated with the expense', + optional: true, + }, + isExpenseBillable: { + type: 'boolean', + description: 'Billable flag', + optional: true, + }, + isPersonalExpense: { + type: 'boolean', + description: 'Personal-expense flag', + optional: true, + }, + isExpenseRejected: { + type: 'boolean', + description: 'Whether the expense was rejected', + optional: true, + }, + isExcludedFromCashAdvanceByUser: { + type: 'boolean', + description: 'Whether the user excluded this from cash advance', + optional: true, + }, + isImageRequired: { + type: 'boolean', + description: 'Whether a receipt image is required', + optional: true, + }, + isPaperReceiptRequired: { + type: 'boolean', + description: 'Whether a paper receipt is required', + optional: true, + }, + isPaperReceiptReceived: { + type: 'boolean', + description: 'Whether a paper receipt was received', + optional: true, + }, + isAutoCreated: { + type: 'boolean', + description: 'Auto-creation indicator', + optional: true, + }, + hasBlockingExceptions: { + type: 'boolean', + description: 'Whether submission-blocking exceptions exist', + optional: true, + }, + hasExceptions: { + type: 'boolean', + description: 'Whether any exceptions exist', + optional: true, + }, + hasMissingReceiptDeclaration: { + type: 'boolean', + description: 'Affidavit declaration status', + optional: true, + }, + attendeeCount: { + type: 'number', + description: 'Number of attendees', + optional: true, + }, + receiptImageId: { + type: 'string', + description: 'Identifier of the attached receipt image', + optional: true, + }, + ereceiptImageId: { + type: 'string', + description: 'eReceipt image identifier', + optional: true, + }, + receiptType: { + type: 'json', + description: 'Receipt {id, status}', + optional: true, + }, + imageCertificationStatus: { + type: 'string', + description: 'Receipt image processing/certification status', + optional: true, + }, + ticketNumber: { + type: 'string', + description: 'Associated travel ticket number', + optional: true, + }, + travel: { + type: 'json', + description: 'Travel data (airline, car rental, hotel, etc.)', + optional: true, + }, + travelAllowance: { + type: 'json', + description: 'Travel allowance association data', + optional: true, + }, + mileage: { + type: 'json', + description: 'Mileage details (odometerStart, odometerEnd, totalDistance, ...)', + optional: true, + }, + expenseTaxSummary: { + type: 'json', + description: 'Aggregated tax data for the expense', + optional: true, + }, + taxRateLocation: { + type: 'string', + description: 'Tax rate location: FOREIGN, HOME, or OUT_OF_PROVINCE', + optional: true, + }, + fuelTypeListItem: { + type: 'json', + description: 'Fuel type list item {id, value, isValid}', + optional: true, + }, + merchantTaxId: { + type: 'string', + description: 'Merchant tax identifier', + optional: true, + }, + customData: { + type: 'json', + description: 'Array of custom field values [{id, value, isValid}]', + optional: true, + }, + parentExpenseId: { + type: 'string', + description: 'Identifier of the parent expense (for itemizations)', + optional: true, + }, + authorizationRequestExpenseId: { + type: 'string', + description: 'Linked travel-request expected expense identifier', + optional: true, + }, + jptRouteId: { + type: 'string', + description: 'Japan Public Transport route id', + optional: true, + }, + invoiceId: { type: 'string', description: 'Invoice identifier', optional: true }, + governmentInvoiceId: { + type: 'string', + description: 'Government invoice identifier', + optional: true, + }, + lastModifiedDate: { + type: 'string', + description: 'Last modified timestamp', + optional: true, + }, + expenseSourceIdentifiers: { + type: 'json', + description: 'Source reference identifiers', + optional: true, + }, + links: { + type: 'json', + description: 'HATEOAS links for the expense', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_expense_report.ts b/apps/sim/tools/sap_concur/get_expense_report.ts new file mode 100644 index 00000000000..9ad031ab5be --- /dev/null +++ b/apps/sim/tools/sap_concur/get_expense_report.ts @@ -0,0 +1,292 @@ +import type { GetExpenseReportParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getExpenseReportTool: ToolConfig = { + id: 'sap_concur_get_expense_report', + name: 'SAP Concur Get Expense Report', + description: + 'Retrieve a single expense report header by id via Expense Report v4 (/expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID who owns the report', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Access context: TRAVELER (own report), MANAGER (report under approval), PROCESSOR, or PROXY', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Concur expense report header (ReportDetails)', + properties: { + reportId: { type: 'string', description: 'Unique report identifier' }, + reportNumber: { type: 'string', description: 'Report number', optional: true }, + reportFormId: { type: 'string', description: 'Report form ID' }, + policyId: { type: 'string', description: 'Policy ID applied to the report' }, + policy: { type: 'string', description: 'Policy name' }, + name: { type: 'string', description: 'Report name' }, + currencyCode: { type: 'string', description: 'ISO currency code' }, + currency: { type: 'string', description: 'Currency name', optional: true }, + approvalStatus: { type: 'string', description: 'Approval status name' }, + approvalStatusId: { type: 'string', description: 'Approval status identifier' }, + paymentStatus: { type: 'string', description: 'Payment status name' }, + paymentStatusId: { type: 'string', description: 'Payment status identifier' }, + ledger: { type: 'string', description: 'Ledger name', optional: true }, + ledgerId: { type: 'string', description: 'Ledger identifier', optional: true }, + userId: { type: 'string', description: 'Owner user UUID' }, + reportDate: { type: 'string', description: 'Report date (YYYY-MM-DD)' }, + creationDate: { type: 'string', description: 'Creation timestamp (ISO 8601)' }, + submitDate: { + type: 'string', + description: 'Submit timestamp (ISO 8601) or null', + optional: true, + }, + startDate: { + type: 'string', + description: 'Report period start (YYYY-MM-DD)', + optional: true, + }, + endDate: { type: 'string', description: 'Report period end (YYYY-MM-DD)', optional: true }, + approvedAmount: { + type: 'json', + description: 'Amount approved { value, currencyCode }', + optional: true, + }, + claimedAmount: { + type: 'json', + description: 'Amount claimed { value, currencyCode }', + optional: true, + }, + reportTotal: { + type: 'json', + description: 'Report total { value, currencyCode }', + optional: true, + }, + amountDueEmployee: { type: 'json', description: 'Amount due employee', optional: true }, + amountDueCompany: { type: 'json', description: 'Amount due company', optional: true }, + amountDueCompanyCard: { + type: 'json', + description: 'Amount due company card', + optional: true, + }, + amountCompanyPaid: { type: 'json', description: 'Amount company has paid', optional: true }, + personalAmount: { + type: 'json', + description: 'Personal portion of the report', + optional: true, + }, + paymentConfirmedAmount: { + type: 'json', + description: 'Confirmed payment amount', + optional: true, + }, + amountNotApproved: { type: 'json', description: 'Amount not approved', optional: true }, + totalAmountPaidEmployee: { + type: 'json', + description: 'Total amount paid to employee', + optional: true, + }, + concurAuditStatus: { type: 'string', description: 'Concur audit status', optional: true }, + isFinancialIntegrationEnabled: { + type: 'boolean', + description: 'Whether financial integration is enabled', + optional: true, + }, + isSubmitted: { + type: 'boolean', + description: 'Whether the report has been submitted', + optional: true, + }, + isSentBack: { + type: 'boolean', + description: 'Whether the report has been sent back', + optional: true, + }, + isReopened: { + type: 'boolean', + description: 'Whether the report was reopened', + optional: true, + }, + isReportEverSentBack: { + type: 'boolean', + description: 'Whether the report was ever sent back', + optional: true, + }, + canRecall: { + type: 'boolean', + description: 'Whether the report can be recalled', + optional: true, + }, + canAddExpense: { + type: 'boolean', + description: 'Whether expenses can be added to the report', + optional: true, + }, + canReopen: { + type: 'boolean', + description: 'Whether the report can be reopened', + optional: true, + }, + isReceiptImageRequired: { + type: 'boolean', + description: 'Whether receipt images are required', + optional: true, + }, + isReceiptImageAvailable: { + type: 'boolean', + description: 'Whether receipt images are available', + optional: true, + }, + isPaperReceiptsReceived: { + type: 'boolean', + description: 'Whether paper receipts were received', + optional: true, + }, + isPendingDelegatorReview: { + type: 'boolean', + description: 'Whether pending delegator review', + optional: true, + }, + isFundsAndGrantsIntegrationEligible: { + type: 'boolean', + description: 'Funds and grants eligibility', + optional: true, + }, + hasReceivedCashAdvanceReturns: { + type: 'boolean', + description: 'Whether cash advance returns received', + optional: true, + }, + analyticsGroupId: { type: 'string', description: 'Analytics group ID', optional: true }, + hierarchyNodeId: { type: 'string', description: 'Hierarchy node ID', optional: true }, + allocationFormId: { type: 'string', description: 'Allocation form ID', optional: true }, + countryCode: { type: 'string', description: 'ISO country code', optional: true }, + countrySubDivisionCode: { + type: 'string', + description: 'ISO country subdivision code', + optional: true, + }, + country: { type: 'string', description: 'Country name', optional: true }, + businessPurpose: { type: 'string', description: 'Business purpose', optional: true }, + comment: { + type: 'string', + description: 'Header-level comment on the report', + optional: true, + }, + reportVersion: { type: 'number', description: 'Report version number', optional: true }, + reportType: { type: 'string', description: 'Report type identifier', optional: true }, + cardProgramStatementPeriodId: { + type: 'string', + description: 'Card program statement period ID', + optional: true, + }, + defaultFieldAccess: { + type: 'string', + description: 'Default field access (HD/RO/RW)', + optional: true, + }, + imageStatus: { type: 'string', description: 'Image status', optional: true }, + receiptContainerId: { type: 'string', description: 'Receipt container ID', optional: true }, + receiptStatus: { type: 'string', description: 'Receipt status', optional: true }, + sponsorId: { type: 'string', description: 'Sponsor ID', optional: true }, + submitterId: { type: 'string', description: 'Submitter user ID', optional: true }, + taxConfigId: { type: 'string', description: 'Tax configuration ID', optional: true }, + redirectFund: { + type: 'json', + description: 'Redirect fund object { amount, creditCardId }', + optional: true, + }, + customData: { + type: 'array', + description: 'Array of custom data { id, value, isValid, listItemUrl }', + optional: true, + }, + employee: { + type: 'json', + description: 'Employee object { employeeId, employeeUuid }', + optional: true, + }, + links: { type: 'array', description: 'HATEOAS links', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_itemizations.ts b/apps/sim/tools/sap_concur/get_itemizations.ts new file mode 100644 index 00000000000..c5724c86872 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_itemizations.ts @@ -0,0 +1,174 @@ +import type { GetItemizationsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getItemizationsTool: ToolConfig = { + id: 'sap_concur_get_itemizations', + name: 'SAP Concur Get Expense Itemizations', + description: + 'Get expense itemizations (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/itemizations).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context (TRAVELER per the v4 spec)', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + expenseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + const expenseId = trimRequired(params.expenseId, 'expenseId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/expenses/${encodeURIComponent(expenseId)}/itemizations`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'array', + description: 'Array of itemizations (ReportExpenseSummary[])', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Itemization identifier', optional: true }, + expenseId: { type: 'string', description: 'Itemization expense id', optional: true }, + allocations: { + type: 'array', + description: 'Allocations applied to the itemization', + optional: true, + }, + expenseType: { + type: 'json', + description: 'Expense type {id, name, code, isDeleted}', + optional: true, + }, + transactionDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + optional: true, + }, + transactionAmount: { type: 'json', description: 'Transaction amount', optional: true }, + postedAmount: { type: 'json', description: 'Posted amount', optional: true }, + approvedAmount: { type: 'json', description: 'Approved amount', optional: true }, + claimedAmount: { type: 'json', description: 'Claimed amount', optional: true }, + approverAdjustedAmount: { + type: 'json', + description: 'Approver-adjusted amount', + optional: true, + }, + paymentType: { type: 'json', description: 'Payment type', optional: true }, + vendor: { type: 'json', description: 'Vendor info', optional: true }, + location: { type: 'json', description: 'Location info', optional: true }, + allocationState: { + type: 'string', + description: 'Allocation state', + optional: true, + }, + allocationSetId: { + type: 'string', + description: 'Allocation set identifier', + optional: true, + }, + attendeeCount: { type: 'number', description: 'Attendee count', optional: true }, + businessPurpose: { + type: 'string', + description: 'Business purpose', + optional: true, + }, + hasBlockingExceptions: { + type: 'boolean', + description: 'Has blocking exceptions', + optional: true, + }, + hasExceptions: { + type: 'boolean', + description: 'Has exceptions', + optional: true, + }, + isPersonalExpense: { + type: 'boolean', + description: 'Personal expense', + optional: true, + }, + links: { type: 'array', description: 'HATEOAS links', optional: true }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_itinerary.ts b/apps/sim/tools/sap_concur/get_itinerary.ts new file mode 100644 index 00000000000..518a34468b4 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_itinerary.ts @@ -0,0 +1,230 @@ +import type { GetItineraryParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getItineraryTool: ToolConfig = { + id: 'sap_concur_get_itinerary', + name: 'SAP Concur Get Trip', + description: 'Get a single trip/itinerary (GET /api/travel/trip/v1.1/{tripID}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + tripId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Trip ID', + }, + useridType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User identifier type (login, xmlsyncid, uuid)', + }, + useridValue: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User identifier value (paired with useridType)', + }, + systemFormat: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional system format (e.g., GDS) for the response', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const tripId = trimRequired(params.tripId, 'tripId') + const query = buildListQuery({ + useridType: params.useridType, + useridValue: params.useridValue, + systemFormat: params.systemFormat, + }) + return { + ...baseProxyBody(params), + path: `/api/travel/trip/v1.1/${encodeURIComponent(tripId)}`, + method: 'GET', + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Trip detail payload (Itinerary v1.1)', + properties: { + ItinLocator: { + type: 'string', + description: 'Concur trip locator (trip ID)', + optional: true, + }, + ClientLocator: { + type: 'string', + description: 'Client (booking source) trip locator', + optional: true, + }, + ItinSourceName: { + type: 'string', + description: 'Booking source name', + optional: true, + }, + BookedVia: { + type: 'string', + description: 'How the trip was booked (e.g. ConcurTravel, Direct)', + optional: true, + }, + TripName: { + type: 'string', + description: 'Trip name', + optional: true, + }, + Status: { + type: 'string', + description: 'Trip status (e.g. Confirmed, Cancelled)', + optional: true, + }, + Description: { + type: 'string', + description: 'Trip description', + optional: true, + }, + Comments: { + type: 'string', + description: 'Comments attached to the trip', + optional: true, + }, + CancelComments: { + type: 'string', + description: 'Cancellation comments (when applicable)', + optional: true, + }, + ProjectName: { + type: 'string', + description: 'Associated project name', + optional: true, + }, + StartDateUtc: { + type: 'string', + description: 'Trip start datetime in UTC', + optional: true, + }, + EndDateUtc: { + type: 'string', + description: 'Trip end datetime in UTC', + optional: true, + }, + StartDateLocal: { + type: 'string', + description: 'Trip start datetime in local time', + optional: true, + }, + EndDateLocal: { + type: 'string', + description: 'Trip end datetime in local time', + optional: true, + }, + DateCreatedUtc: { + type: 'string', + description: 'Trip creation timestamp (UTC)', + optional: true, + }, + DateModifiedUtc: { + type: 'string', + description: 'Trip last-modified timestamp (UTC)', + optional: true, + }, + DateBookedLocal: { + type: 'string', + description: 'Booking date in local time', + optional: true, + }, + UserLoginId: { + type: 'string', + description: 'Login id of the trip owner', + optional: true, + }, + BookedByFirstName: { + type: 'string', + description: 'First name of the booker', + optional: true, + }, + BookedByLastName: { + type: 'string', + description: 'Last name of the booker', + optional: true, + }, + IsPersonal: { + type: 'boolean', + description: 'Whether the trip is flagged personal', + optional: true, + }, + RuleViolations: { + type: 'array', + description: 'Travel rule violations attached to the trip', + optional: true, + items: { type: 'json' }, + }, + Bookings: { + type: 'array', + description: 'Bookings (air/hotel/car/rail) attached to the trip', + optional: true, + items: { type: 'json' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_list.ts b/apps/sim/tools/sap_concur/get_list.ts new file mode 100644 index 00000000000..3c6030614dd --- /dev/null +++ b/apps/sim/tools/sap_concur/get_list.ts @@ -0,0 +1,134 @@ +import type { GetListParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getListTool: ToolConfig = { + id: 'sap_concur_get_list', + name: 'SAP Concur Get List', + description: 'Get a single custom list (GET /list/v4/lists/{listId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + listId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'List ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const listId = trimRequired(params.listId, 'listId') + return { + ...baseProxyBody(params), + path: `/list/v4/lists/${encodeURIComponent(listId)}`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'List detail payload', + properties: { + id: { type: 'string', description: 'Unique identifier (UUID) of the list', optional: true }, + value: { type: 'string', description: 'Name of the list', optional: true }, + levelCount: { + type: 'number', + description: 'Number of levels in the list', + optional: true, + }, + searchCriteria: { + type: 'string', + description: 'Search attribute (TEXT or CODE)', + optional: true, + }, + displayFormat: { + type: 'string', + description: 'Display order ((CODE) TEXT or TEXT (CODE))', + optional: true, + }, + category: { + type: 'json', + description: 'List category', + optional: true, + properties: { + id: { type: 'string', description: 'Category UUID', optional: true }, + type: { type: 'string', description: 'Category type', optional: true }, + }, + }, + isReadOnly: { + type: 'boolean', + description: 'Whether the list is read-only', + optional: true, + }, + isDeleted: { + type: 'boolean', + description: 'Whether the list has been deleted', + optional: true, + }, + managedBy: { + type: 'string', + description: 'Identifier of the managing application or service', + optional: true, + }, + externalThreshold: { + type: 'number', + description: 'Threshold from where the level starts being external', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_list_item.ts b/apps/sim/tools/sap_concur/get_list_item.ts new file mode 100644 index 00000000000..611844a8bbc --- /dev/null +++ b/apps/sim/tools/sap_concur/get_list_item.ts @@ -0,0 +1,123 @@ +import type { GetListItemParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getListItemTool: ToolConfig = { + id: 'sap_concur_get_list_item', + name: 'SAP Concur Get List Item', + description: 'Get a single list item (GET /list/v4/items/{itemId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'List item ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const itemId = trimRequired(params.itemId, 'itemId') + return { + ...baseProxyBody(params), + path: `/list/v4/items/${encodeURIComponent(itemId)}`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'List item detail payload', + properties: { + id: { type: 'string', description: 'List item UUID', optional: true }, + code: { type: 'string', description: 'Long code format for the item', optional: true }, + shortCode: { type: 'string', description: 'Short code identifier', optional: true }, + value: { type: 'string', description: 'Display value of the item', optional: true }, + parentId: { + type: 'string', + description: 'Parent item UUID (omitted for first-level items)', + optional: true, + }, + level: { + type: 'number', + description: 'Hierarchy level (1 for root items)', + optional: true, + }, + isDeleted: { + type: 'boolean', + description: 'Deletion status across all containing lists', + optional: true, + }, + lists: { + type: 'array', + description: 'Lists containing this item', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'List UUID', optional: true }, + hasChildren: { + type: 'boolean', + description: 'Whether this item has children in the list', + optional: true, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_purchase_request.ts b/apps/sim/tools/sap_concur/get_purchase_request.ts new file mode 100644 index 00000000000..e9f0eefb1ba --- /dev/null +++ b/apps/sim/tools/sap_concur/get_purchase_request.ts @@ -0,0 +1,155 @@ +import type { GetPurchaseRequestParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getPurchaseRequestTool: ToolConfig = + { + id: 'sap_concur_get_purchase_request', + name: 'SAP Concur Get Purchase Request', + description: 'Get a purchase request by ID (GET /purchaserequest/v4/purchaserequests/{id}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + purchaseRequestId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Purchase request ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const purchaseRequestId = trimRequired(params.purchaseRequestId, 'purchaseRequestId') + return { + ...baseProxyBody(params), + path: `/purchaserequest/v4/purchaserequests/${encodeURIComponent(purchaseRequestId)}?mode=COMPACT`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Purchase request detail payload', + properties: { + purchaseRequestId: { + type: 'string', + description: 'Unique identifier of the purchase request', + optional: true, + }, + purchaseRequestNumber: { + type: 'string', + description: 'Human-readable purchase request number', + optional: true, + }, + purchaseRequestQueueStatus: { + type: 'string', + description: 'Queue status of the purchase request', + optional: true, + }, + purchaseRequestWorkflowStatus: { + type: 'string', + description: 'Workflow status of the purchase request', + optional: true, + }, + purchaseOrders: { + type: 'array', + description: 'Purchase orders generated from the request', + optional: true, + items: { + type: 'json', + properties: { + purchaseOrderNumber: { + type: 'string', + description: 'Purchase order number', + optional: true, + }, + }, + }, + }, + purchaseRequestExceptions: { + type: 'array', + description: 'Exceptions raised on the purchase request', + optional: true, + items: { + type: 'json', + properties: { + eventCode: { type: 'string', description: 'Event code', optional: true }, + exceptionCode: { + type: 'string', + description: 'Exception code', + optional: true, + }, + isCleared: { + type: 'boolean', + description: 'Whether the exception has been cleared', + optional: true, + }, + prExceptionId: { + type: 'string', + description: 'Identifier of the exception record', + optional: true, + }, + message: { + type: 'string', + description: 'Exception message', + optional: true, + }, + }, + }, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/get_receipt.ts b/apps/sim/tools/sap_concur/get_receipt.ts new file mode 100644 index 00000000000..95e1bced682 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_receipt.ts @@ -0,0 +1,120 @@ +import type { GetReceiptParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getReceiptTool: ToolConfig = { + id: 'sap_concur_get_receipt', + name: 'SAP Concur Get Receipt', + description: 'Get a single receipt by ID (GET /receipts/v4/{receiptId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + receiptId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Receipt ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const receiptId = trimRequired(params.receiptId, 'receiptId') + return { + ...baseProxyBody(params), + path: `/receipts/v4/${encodeURIComponent(receiptId)}`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Receipt detail payload', + properties: { + id: { type: 'string', description: 'Receipt identifier', optional: true }, + userId: { type: 'string', description: 'Owning user UUID', optional: true }, + dateTimeReceived: { + type: 'string', + description: 'Timestamp when the receipt was received (ISO 8601)', + optional: true, + }, + receipt: { + type: 'json', + description: 'Parsed receipt JSON object', + optional: true, + }, + image: { + type: 'string', + description: 'Receipt image URL or data reference', + optional: true, + }, + validationSchema: { + type: 'string', + description: 'Schema used to validate the receipt', + optional: true, + }, + self: { + type: 'string', + description: 'URL to this receipt resource', + optional: true, + }, + template: { + type: 'string', + description: 'URL template for receipts', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_receipt_status.ts b/apps/sim/tools/sap_concur/get_receipt_status.ts new file mode 100644 index 00000000000..10216d37a7b --- /dev/null +++ b/apps/sim/tools/sap_concur/get_receipt_status.ts @@ -0,0 +1,106 @@ +import type { GetReceiptStatusParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getReceiptStatusTool: ToolConfig = { + id: 'sap_concur_get_receipt_status', + name: 'SAP Concur Get Receipt Status', + description: 'Get receipt processing status (GET /receipts/v4/status/{receiptId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + receiptId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Receipt ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const receiptId = trimRequired(params.receiptId, 'receiptId') + return { + ...baseProxyBody(params), + path: `/receipts/v4/status/${encodeURIComponent(receiptId)}`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Receipt status payload', + properties: { + status: { + type: 'string', + description: 'Processing status: ACCEPTED, PROCESSING, PROCESSED, or FAILED', + optional: true, + }, + logs: { + type: 'array', + description: 'Array of log entries', + optional: true, + items: { + type: 'json', + properties: { + logLevel: { type: 'string', description: 'Log level', optional: true }, + message: { type: 'string', description: 'Log message', optional: true }, + timestamp: { type: 'string', description: 'Log timestamp', optional: true }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_request_cash_advance.ts b/apps/sim/tools/sap_concur/get_request_cash_advance.ts new file mode 100644 index 00000000000..4f923fb98b7 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_request_cash_advance.ts @@ -0,0 +1,130 @@ +import type { GetRequestCashAdvanceParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getRequestCashAdvanceTool: ToolConfig< + GetRequestCashAdvanceParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_get_request_cash_advance', + name: 'SAP Concur Get Request Cash Advance', + description: + 'Get a single cash advance assigned to a travel request (GET /travelrequest/v4/cashadvances/{cashAdvanceUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + cashAdvanceUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Cash advance UUID (returned as part of a travel request)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const cashAdvanceUuid = trimRequired(params.cashAdvanceUuid, 'cashAdvanceUuid') + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/cashadvances/${encodeURIComponent(cashAdvanceUuid)}`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Cash advance detail', + properties: { + cashAdvanceId: { + type: 'string', + description: 'Unique cash advance identifier', + optional: true, + }, + amountRequested: { + type: 'json', + description: 'Requested amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + amount: { type: 'number', description: 'Amount (alias)', optional: true }, + }, + }, + approvalStatus: { + type: 'json', + description: 'Approval status', + optional: true, + properties: { + code: { type: 'string', description: 'Status code', optional: true }, + name: { type: 'string', description: 'Status name', optional: true }, + }, + }, + requestDate: { + type: 'string', + description: 'Request datetime (ISO 8601)', + optional: true, + }, + exchangeRate: { + type: 'json', + description: 'Exchange rate', + optional: true, + properties: { + value: { type: 'number', description: 'Rate value', optional: true }, + operation: { type: 'string', description: 'Multiply or divide', optional: true }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_travel_profile.ts b/apps/sim/tools/sap_concur/get_travel_profile.ts new file mode 100644 index 00000000000..9bc774b70d7 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_travel_profile.ts @@ -0,0 +1,234 @@ +import type { GetTravelProfileParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getTravelProfileTool: ToolConfig = { + id: 'sap_concur_get_travel_profile', + name: 'SAP Concur Get Travel Profile', + description: + 'Get a travel profile (GET /api/travelprofile/v2.0/profile). Returns the calling user by default; pass userid_type and userid_value to impersonate.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + useridType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Identifier type: login, xmlsyncid, or uuid', + }, + useridValue: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Identifier value (login id, xml sync id, or UUID)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const query = buildListQuery({ + userid_type: params.useridType, + userid_value: params.useridValue, + }) + return { + ...baseProxyBody(params), + path: '/api/travelprofile/v2.0/profile', + method: 'GET', + query: Object.keys(query).length > 0 ? query : undefined, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: + 'Travel profile payload. Concur returns XML; downstream may parse it to a best-effort JSON object with the documented top-level sections.', + properties: { + General: { + type: 'json', + description: + 'General profile info (NamePrefix, FirstName, MiddleName, LastName, NameSuffix, JobTitle, CompanyEmployeeID, EmailAddress, RuleClass, TravelConfigID, etc.)', + optional: true, + }, + Telephones: { + type: 'json', + description: 'Telephone numbers (Telephone[] with Type, CountryCode, PhoneNumber, etc.)', + optional: true, + }, + Addresses: { + type: 'json', + description: 'Address records (Address[] with Type, Street, City, StateProvince, etc.)', + optional: true, + }, + DriversLicenses: { + type: 'array', + description: 'Drivers license records', + optional: true, + items: { type: 'json' }, + }, + NationalIDs: { + type: 'array', + description: 'National ID records', + optional: true, + items: { type: 'json' }, + }, + EmailAddresses: { + type: 'json', + description: 'Email addresses (EmailAddress[] with Type, Address, Contact, Verified)', + optional: true, + }, + EmergencyContact: { + type: 'json', + description: 'Emergency contact (Name, Relationship, Phones, Address)', + optional: true, + }, + Air: { + type: 'json', + description: 'Air travel preferences (HomeAirport, Seat, Meal, AirOther, AirMemberships)', + optional: true, + }, + Rail: { + type: 'json', + description: 'Rail preferences (Seat, Coach, Berth, Other, RailMemberships)', + optional: true, + }, + Hotel: { + type: 'json', + description: + 'Hotel preferences (SmokingCode, RoomType, HotelOther, HotelMemberships, Accessibility flags)', + optional: true, + }, + Car: { + type: 'json', + description: 'Car rental preferences (CarSmokingCode, CarType, CarMemberships, etc.)', + optional: true, + }, + CustomFields: { + type: 'json', + description: 'Custom-defined fields configured by the company', + optional: true, + }, + RatePreferences: { + type: 'json', + description: 'Rate preferences (e.g. AAA, AARP, government, military rates)', + optional: true, + }, + DiscountCodes: { + type: 'json', + description: 'Discount codes available to the traveler', + optional: true, + }, + HasNoPassport: { + type: 'boolean', + description: 'Whether the traveler has no passport on file', + optional: true, + }, + Roles: { + type: 'json', + description: 'Role assignments (TravelManager, Assistant, etc.)', + optional: true, + }, + Sponsors: { + type: 'json', + description: 'Sponsor information for guest travelers', + optional: true, + }, + TSAInfo: { + type: 'json', + description: 'TSA SecureFlight info (Gender, DateOfBirth, NoMiddleName, etc.)', + optional: true, + }, + Passports: { + type: 'json', + description: 'Passport documents (Passport[] with PassportNumber, Country, Expiration)', + optional: true, + }, + Visas: { + type: 'json', + description: 'Visa documents (Visa[] with VisaNationality, VisaNumber, etc.)', + optional: true, + }, + UnusedTickets: { + type: 'json', + description: 'Unused ticket records', + optional: true, + }, + SouthwestUnusedTickets: { + type: 'json', + description: 'Southwest-specific unused ticket records', + optional: true, + }, + AdvantageMemberships: { + type: 'json', + description: 'Advantage program memberships', + optional: true, + }, + XmlSyncId: { + type: 'string', + description: 'XML sync identifier for the user', + optional: true, + }, + LoginId: { + type: 'string', + description: 'Concur login id', + optional: true, + }, + ProfileLastModifiedUTC: { + type: 'string', + description: 'UTC timestamp the profile was last modified', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_travel_request.ts b/apps/sim/tools/sap_concur/get_travel_request.ts new file mode 100644 index 00000000000..a46831ffc0c --- /dev/null +++ b/apps/sim/tools/sap_concur/get_travel_request.ts @@ -0,0 +1,336 @@ +import type { GetTravelRequestParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getTravelRequestTool: ToolConfig = { + id: 'sap_concur_get_travel_request', + name: 'SAP Concur Get Travel Request', + description: 'Get a single travel request (GET /travelrequest/v4/requests/{requestUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + requestUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Travel request UUID', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Concur user UUID — required when impersonating another user', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const requestUuid = trimRequired(params.requestUuid, 'requestUuid') + const query: Record = {} + if (params.userId) query.userId = params.userId + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests/${encodeURIComponent(requestUuid)}`, + method: 'GET', + query: Object.keys(query).length > 0 ? query : undefined, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Travel request detail payload', + properties: { + id: { type: 'string', description: 'Travel request UUID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + requestId: { + type: 'string', + description: 'Public-facing request ID (4-6 alphanumeric characters)', + optional: true, + }, + name: { type: 'string', description: 'Request name', optional: true }, + businessPurpose: { type: 'string', description: 'Business purpose', optional: true }, + comment: { type: 'string', description: 'Last attached comment', optional: true }, + creationDate: { type: 'string', description: 'Creation timestamp', optional: true }, + lastModified: { + type: 'string', + description: 'Last modification timestamp', + optional: true, + }, + submitDate: { type: 'string', description: 'Last submission timestamp', optional: true }, + authorizedDate: { + type: 'string', + description: 'Date when approval was completed', + optional: true, + }, + approvalLimitDate: { + type: 'string', + description: 'Required approval deadline', + optional: true, + }, + startDate: { type: 'string', description: 'Trip start date (ISO 8601)', optional: true }, + endDate: { type: 'string', description: 'Trip end date (ISO 8601)', optional: true }, + startTime: { type: 'string', description: 'Trip start time (HH:mm)', optional: true }, + endTime: { type: 'string', description: 'Trip end time (HH:mm)', optional: true }, + pnr: { type: 'string', description: 'Passenger record number', optional: true }, + approved: { + type: 'boolean', + description: 'Whether the request is approved', + optional: true, + }, + pendingApproval: { type: 'boolean', description: 'Pending approval flag', optional: true }, + closed: { type: 'boolean', description: 'Closed flag', optional: true }, + everSentBack: { type: 'boolean', description: 'Ever-sent-back flag', optional: true }, + canceledPostApproval: { + type: 'boolean', + description: 'Canceled after approval flag', + optional: true, + }, + isParentRequest: { type: 'boolean', description: 'Parent request flag', optional: true }, + parentRequestId: { + type: 'string', + description: 'Parent budget request ID', + optional: true, + }, + allocationFormId: { + type: 'string', + description: 'Allocation form identifier', + optional: true, + }, + highestExceptionLevel: { + type: 'string', + description: 'Highest exception level (WARNING, ERROR, NONE)', + optional: true, + }, + approvalStatus: { + type: 'json', + description: 'Approval status', + optional: true, + properties: { + code: { + type: 'string', + description: 'Status code (NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK)', + optional: true, + }, + name: { type: 'string', description: 'Localized status name', optional: true }, + }, + }, + owner: { + type: 'json', + description: 'Travel request owner', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { type: 'string', description: 'Owner first name', optional: true }, + lastName: { type: 'string', description: 'Owner last name', optional: true }, + }, + }, + approver: { + type: 'json', + description: 'Approver assigned to the request', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { type: 'string', description: 'Approver first name', optional: true }, + lastName: { type: 'string', description: 'Approver last name', optional: true }, + }, + }, + policy: { + type: 'json', + description: 'Resource link to the applicable policy', + optional: true, + properties: { + id: { type: 'string', description: 'Policy ID', optional: true }, + href: { type: 'string', description: 'Policy hyperlink', optional: true }, + }, + }, + type: { + type: 'json', + description: 'Request type', + optional: true, + properties: { + code: { type: 'string', description: 'Request type code', optional: true }, + label: { type: 'string', description: 'Request type label', optional: true }, + }, + }, + mainDestination: { + type: 'json', + description: 'Main destination of the trip', + optional: true, + properties: { + city: { type: 'string', description: 'City', optional: true }, + countryCode: { type: 'string', description: 'ISO country code', optional: true }, + countrySubDivisionCode: { + type: 'string', + description: 'ISO country sub-division code', + optional: true, + }, + name: { type: 'string', description: 'Destination name', optional: true }, + }, + }, + totalApprovedAmount: { + type: 'json', + description: 'Total approved amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + totalPostedAmount: { + type: 'json', + description: 'Total posted amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + totalRemainingAmount: { + type: 'json', + description: 'Total remaining amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + expenses: { + type: 'array', + description: 'Resource links to expected expenses', + optional: true, + items: { type: 'json' }, + }, + cashAdvances: { + type: 'json', + description: 'Resource link to cash advances', + optional: true, + properties: { + id: { type: 'string', description: 'Resource ID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + }, + }, + comments: { + type: 'json', + description: 'Resource link to comments', + optional: true, + properties: { + id: { type: 'string', description: 'Resource ID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + }, + }, + exceptions: { + type: 'json', + description: 'Resource link to exceptions', + optional: true, + properties: { + id: { type: 'string', description: 'Resource ID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + }, + }, + travelAgency: { + type: 'json', + description: 'Resource link to travel agency', + optional: true, + properties: { + id: { type: 'string', description: 'Resource ID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + }, + }, + parentRequest: { + type: 'json', + description: 'Resource link to parent request', + optional: true, + properties: { + id: { type: 'string', description: 'Resource ID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + }, + }, + eventRequest: { + type: 'json', + description: 'Resource link to parent event request', + optional: true, + properties: { + id: { type: 'string', description: 'Resource ID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + }, + }, + operations: { + type: 'array', + description: 'Available workflow actions', + optional: true, + items: { + type: 'json', + properties: { + rel: { type: 'string', description: 'Operation name', optional: true }, + href: { type: 'string', description: 'Operation URL', optional: true }, + }, + }, + }, + expensePolicy: { + type: 'json', + description: 'Expense policy reference', + optional: true, + properties: { + id: { type: 'string', description: 'Policy identifier', optional: true }, + href: { type: 'string', description: 'Policy URL', optional: true }, + }, + }, + custom1: { type: 'json', description: 'Custom field 1', optional: true }, + custom2: { type: 'json', description: 'Custom field 2', optional: true }, + custom3: { type: 'json', description: 'Custom field 3', optional: true }, + custom4: { type: 'json', description: 'Custom field 4', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_user.ts b/apps/sim/tools/sap_concur/get_user.ts new file mode 100644 index 00000000000..c8aecbd787d --- /dev/null +++ b/apps/sim/tools/sap_concur/get_user.ts @@ -0,0 +1,105 @@ +import type { GetUserParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + scimUserOutputProperties, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getUserTool: ToolConfig = { + id: 'sap_concur_get_user', + name: 'SAP Concur Get User', + description: 'Get a single user by UUID (GET /profile/identity/v4.1/Users/{id}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User UUID', + }, + attributes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated SCIM attributes to include in the response', + }, + excludedAttributes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated SCIM attributes to exclude from the response', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userUuid = trimRequired(params.userUuid, 'userUuid') + const query: Record = {} + if (params.attributes?.trim()) query.attributes = params.attributes.trim() + if (params.excludedAttributes?.trim()) + query.excludedAttributes = params.excludedAttributes.trim() + return { + ...baseProxyBody(params), + path: `/profile/identity/v4.1/Users/${encodeURIComponent(userUuid)}`, + method: 'GET', + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'SCIM User identity payload', + properties: scimUserOutputProperties, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/index.ts b/apps/sim/tools/sap_concur/index.ts new file mode 100644 index 00000000000..621496eebe5 --- /dev/null +++ b/apps/sim/tools/sap_concur/index.ts @@ -0,0 +1,70 @@ +export { approveExpenseReportTool } from '@/tools/sap_concur/approve_expense_report' +export { associateAttendeesTool } from '@/tools/sap_concur/associate_attendees' +export { createCashAdvanceTool } from '@/tools/sap_concur/create_cash_advance' +export { createExpectedExpenseTool } from '@/tools/sap_concur/create_expected_expense' +export { createExpenseReportTool } from '@/tools/sap_concur/create_expense_report' +export { createListItemTool } from '@/tools/sap_concur/create_list_item' +export { createPurchaseRequestTool } from '@/tools/sap_concur/create_purchase_request' +export { createQuickExpenseTool } from '@/tools/sap_concur/create_quick_expense' +export { createQuickExpenseWithImageTool } from '@/tools/sap_concur/create_quick_expense_with_image' +export { createReportCommentTool } from '@/tools/sap_concur/create_report_comment' +export { createTravelRequestTool } from '@/tools/sap_concur/create_travel_request' +export { createUserTool } from '@/tools/sap_concur/create_user' +export { deleteExpectedExpenseTool } from '@/tools/sap_concur/delete_expected_expense' +export { deleteExpenseTool } from '@/tools/sap_concur/delete_expense' +export { deleteExpenseReportTool } from '@/tools/sap_concur/delete_expense_report' +export { deleteListItemTool } from '@/tools/sap_concur/delete_list_item' +export { deleteTravelRequestTool } from '@/tools/sap_concur/delete_travel_request' +export { deleteUserTool } from '@/tools/sap_concur/delete_user' +export { getAllocationTool } from '@/tools/sap_concur/get_allocation' +export { getBudgetTool } from '@/tools/sap_concur/get_budget' +export { getCashAdvanceTool } from '@/tools/sap_concur/get_cash_advance' +export { getExchangeRateTool } from '@/tools/sap_concur/get_exchange_rate' +export { getExpectedExpenseTool } from '@/tools/sap_concur/get_expected_expense' +export { getExpenseTool } from '@/tools/sap_concur/get_expense' +export { getExpenseReportTool } from '@/tools/sap_concur/get_expense_report' +export { getItemizationsTool } from '@/tools/sap_concur/get_itemizations' +export { getItineraryTool } from '@/tools/sap_concur/get_itinerary' +export { getListTool } from '@/tools/sap_concur/get_list' +export { getListItemTool } from '@/tools/sap_concur/get_list_item' +export { getPurchaseRequestTool } from '@/tools/sap_concur/get_purchase_request' +export { getReceiptTool } from '@/tools/sap_concur/get_receipt' +export { getReceiptStatusTool } from '@/tools/sap_concur/get_receipt_status' +export { getRequestCashAdvanceTool } from '@/tools/sap_concur/get_request_cash_advance' +export { getTravelProfileTool } from '@/tools/sap_concur/get_travel_profile' +export { getTravelRequestTool } from '@/tools/sap_concur/get_travel_request' +export { getUserTool } from '@/tools/sap_concur/get_user' +export { issueCashAdvanceTool } from '@/tools/sap_concur/issue_cash_advance' +export { listAllocationsTool } from '@/tools/sap_concur/list_allocations' +export { listAttendeeAssociationsTool } from '@/tools/sap_concur/list_attendee_associations' +export { listBudgetCategoriesTool } from '@/tools/sap_concur/list_budget_categories' +export { listBudgetsTool } from '@/tools/sap_concur/list_budgets' +export { listExceptionsTool } from '@/tools/sap_concur/list_exceptions' +export { listExpectedExpensesTool } from '@/tools/sap_concur/list_expected_expenses' +export { listExpenseReportsTool } from '@/tools/sap_concur/list_expense_reports' +export { listExpensesTool } from '@/tools/sap_concur/list_expenses' +export { listItinerariesTool } from '@/tools/sap_concur/list_itineraries' +export { listListItemsTool } from '@/tools/sap_concur/list_list_items' +export { listListsTool } from '@/tools/sap_concur/list_lists' +export { listReceiptsTool } from '@/tools/sap_concur/list_receipts' +export { listReportCommentsTool } from '@/tools/sap_concur/list_report_comments' +export { listReportsToApproveTool } from '@/tools/sap_concur/list_reports_to_approve' +export { listTravelProfilesSummaryTool } from '@/tools/sap_concur/list_travel_profiles_summary' +export { listTravelRequestCommentsTool } from '@/tools/sap_concur/list_travel_request_comments' +export { listTravelRequestsTool } from '@/tools/sap_concur/list_travel_requests' +export { listUsersTool } from '@/tools/sap_concur/list_users' +export { moveTravelRequestTool } from '@/tools/sap_concur/move_travel_request' +export { recallExpenseReportTool } from '@/tools/sap_concur/recall_expense_report' +export { removeAllAttendeesTool } from '@/tools/sap_concur/remove_all_attendees' +export { searchLocationsTool } from '@/tools/sap_concur/search_locations' +export { searchUsersTool } from '@/tools/sap_concur/search_users' +export { sendBackExpenseReportTool } from '@/tools/sap_concur/send_back_expense_report' +export { submitExpenseReportTool } from '@/tools/sap_concur/submit_expense_report' +export { updateAllocationTool } from '@/tools/sap_concur/update_allocation' +export { updateExpectedExpenseTool } from '@/tools/sap_concur/update_expected_expense' +export { updateExpenseTool } from '@/tools/sap_concur/update_expense' +export { updateExpenseReportTool } from '@/tools/sap_concur/update_expense_report' +export { updateListItemTool } from '@/tools/sap_concur/update_list_item' +export { updateTravelRequestTool } from '@/tools/sap_concur/update_travel_request' +export { updateUserTool } from '@/tools/sap_concur/update_user' +export { uploadReceiptImageTool } from '@/tools/sap_concur/upload_receipt_image' diff --git a/apps/sim/tools/sap_concur/issue_cash_advance.ts b/apps/sim/tools/sap_concur/issue_cash_advance.ts new file mode 100644 index 00000000000..527edaf9a7c --- /dev/null +++ b/apps/sim/tools/sap_concur/issue_cash_advance.ts @@ -0,0 +1,109 @@ +import type { IssueCashAdvanceParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const issueCashAdvanceTool: ToolConfig = { + id: 'sap_concur_issue_cash_advance', + name: 'SAP Concur Issue Cash Advance', + description: 'Issue a cash advance (POST /cashadvance/v4/cashadvances/{cashadvanceId}/issue).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + cashAdvanceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Cash advance ID to issue', + }, + body: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Optional request body', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const cashAdvanceId = trimRequired(params.cashAdvanceId, 'cashAdvanceId') + return { + ...baseProxyBody(params), + path: `/cashadvance/v4/cashadvances/${encodeURIComponent(cashAdvanceId)}/issue`, + method: 'POST', + body: params.body ?? {}, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Issue cash advance result payload', + properties: { + issuedDate: { + type: 'string', + description: 'Date the cash advance was issued (YYYY-MM-DD)', + optional: true, + }, + status: { + type: 'json', + description: 'Cash advance status after the issue action', + optional: true, + properties: { + code: { type: 'string', description: 'Status code', optional: true }, + name: { type: 'string', description: 'Status display name', optional: true }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_allocations.ts b/apps/sim/tools/sap_concur/list_allocations.ts new file mode 100644 index 00000000000..d02968fe2e3 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_allocations.ts @@ -0,0 +1,117 @@ +import type { ListAllocationsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listAllocationsTool: ToolConfig = { + id: 'sap_concur_list_allocations', + name: 'SAP Concur List Allocations', + description: + 'List allocations on an expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/allocations).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: TRAVELER or PROXY', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + expenseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + const expenseId = trimRequired(params.expenseId, 'expenseId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/expenses/${encodeURIComponent(expenseId)}/allocations`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Allocations list payload', + properties: { + items: { + type: 'array', + optional: true, + description: + 'Array of allocation objects (allocationId, accountCode, percentage, allocationAmount, approvedAmount, claimedAmount, customData, expenseId, isSystemAllocation, isPercentEdited, overLimitAccountCode)', + items: { type: 'json' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_attendee_associations.ts b/apps/sim/tools/sap_concur/list_attendee_associations.ts new file mode 100644 index 00000000000..a4d16dbe096 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_attendee_associations.ts @@ -0,0 +1,193 @@ +import type { + ListAttendeeAssociationsParams, + SapConcurProxyResponse, +} from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listAttendeeAssociationsTool: ToolConfig< + ListAttendeeAssociationsParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_list_attendee_associations', + name: 'SAP Concur List Attendee Associations', + description: + 'List attendees associated with an expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/attendees).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: TRAVELER or PROXY', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + expenseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + const expenseId = trimRequired(params.expenseId, 'expenseId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/expenses/${encodeURIComponent(expenseId)}/attendees`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Attendees list payload', + properties: { + noShowAttendeeCount: { + type: 'number', + description: 'Number of unnamed/no-show attendees', + optional: true, + }, + expenseAttendeeList: { + type: 'array', + description: 'Attendees associated with the expense, including amounts', + items: { + type: 'json', + properties: { + attendeeId: { type: 'string', description: 'Unique identifier of the attendee' }, + transactionAmount: { + type: 'json', + description: 'Expense portion assigned to this attendee', + properties: { + value: { type: 'number', description: 'Numeric amount' }, + currencyCode: { type: 'string', description: 'ISO 4217 currency code' }, + }, + }, + approvedAmount: { + type: 'json', + description: 'Approved amount in report currency', + properties: { + value: { type: 'number', description: 'Numeric amount' }, + currencyCode: { type: 'string', description: 'ISO 4217 currency code' }, + }, + }, + isAmountUserEdited: { + type: 'boolean', + description: 'Whether the amount was manually edited', + optional: true, + }, + isTraveling: { + type: 'boolean', + description: 'Whether the attendee is traveling (affects tax calculations)', + optional: true, + }, + associatedAttendeeCount: { + type: 'number', + description: 'Total attendee count; greater than 1 indicates unnamed attendees', + optional: true, + }, + versionNumber: { + type: 'number', + description: 'Version number preserving previous attendee state', + optional: true, + }, + customData: { + type: 'array', + description: 'Custom field values for the association', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Custom field identifier' }, + value: { + type: 'string', + description: 'Custom field value (max 48 characters)', + optional: true, + }, + isValid: { + type: 'boolean', + description: 'Whether the value passes validation', + optional: true, + }, + listItemUrl: { + type: 'string', + description: 'HATEOAS link for list items', + optional: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_budget_categories.ts b/apps/sim/tools/sap_concur/list_budget_categories.ts new file mode 100644 index 00000000000..ed713c249b6 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_budget_categories.ts @@ -0,0 +1,106 @@ +import type { ListBudgetCategoriesParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listBudgetCategoriesTool: ToolConfig< + ListBudgetCategoriesParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_list_budget_categories', + name: 'SAP Concur List Budget Categories', + description: 'List budget categories (GET /budget/v4/budgetCategory).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + ...baseProxyBody(params), + path: `/budget/v4/budgetCategory`, + method: 'GET', + }), + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Budget categories collection payload', + properties: { + items: { + type: 'array', + optional: true, + description: 'Array of budget category objects', + items: { + type: 'json', + properties: { + id: { type: 'string', optional: true, description: 'Category ID' }, + name: { type: 'string', optional: true, description: 'Admin-facing category name' }, + description: { type: 'string', optional: true, description: 'Friendly name' }, + statusType: { + type: 'string', + optional: true, + description: 'Status: OPEN or REMOVED', + }, + expenseTypes: { + type: 'array', + optional: true, + description: + 'Expense types in this category (id, featureTypeCode, expenseTypeCode, name)', + items: { type: 'json' }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_budgets.ts b/apps/sim/tools/sap_concur/list_budgets.ts new file mode 100644 index 00000000000..d82ecdc25fe --- /dev/null +++ b/apps/sim/tools/sap_concur/list_budgets.ts @@ -0,0 +1,112 @@ +import type { ListBudgetsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listBudgetsTool: ToolConfig = { + id: 'sap_concur_list_budgets', + name: 'SAP Concur List Budgets', + description: 'List budget item headers (GET /budget/v4/budgetItemHeader).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + adminView: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'When true, returns all budgets the caller can administer (default false)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page offset (Concur returns up to 50 budget headers per page)', + }, + responseSchema: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Response schema variant: "COMPACT" returns a smaller payload', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + ...baseProxyBody(params), + path: `/budget/v4/budgetItemHeader`, + method: 'GET', + query: buildListQuery({ + adminView: params.adminView, + offset: params.offset, + responseSchema: params.responseSchema, + }), + }), + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Budget headers collection payload', + properties: { + items: { + type: 'array', + optional: true, + description: + 'Array of budget item header summaries (id, name, description, budgetItemStatusType, budgetType, currencyCode, fiscalYear, budgetAmounts, owner, ...)', + items: { type: 'json' }, + }, + offset: { type: 'number', optional: true, description: 'Page offset' }, + limit: { type: 'number', optional: true, description: 'Page size' }, + totalCount: { type: 'number', optional: true, description: 'Total result count' }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_exceptions.ts b/apps/sim/tools/sap_concur/list_exceptions.ts new file mode 100644 index 00000000000..670ad5ae139 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_exceptions.ts @@ -0,0 +1,131 @@ +import type { ListExceptionsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listExceptionsTool: ToolConfig = { + id: 'sap_concur_list_exceptions', + name: 'SAP Concur List Report Exceptions', + description: + 'List exceptions on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/exceptions).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: TRAVELER, MANAGER, or PROXY', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/exceptions`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'array', + description: 'Array of report header exception entries', + items: { + type: 'json', + properties: { + exceptionCode: { type: 'string', description: 'Unique exception code' }, + exceptionVisibility: { + type: 'string', + description: 'Visibility scope: ALL, APPROVER_PROCESSOR, or PROCESSOR', + }, + isBlocking: { + type: 'boolean', + description: 'Whether the exception prevents report submission', + }, + message: { type: 'string', description: 'Human-readable description of the exception' }, + expenseId: { + type: 'string', + description: 'Related expense entry ID', + optional: true, + }, + allocationId: { + type: 'string', + description: 'Related allocation ID, if any', + optional: true, + }, + parentExpenseId: { + type: 'string', + description: 'Parent expense ID for itemized entries', + optional: true, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_expected_expenses.ts b/apps/sim/tools/sap_concur/list_expected_expenses.ts new file mode 100644 index 00000000000..ba2c42a9d9c --- /dev/null +++ b/apps/sim/tools/sap_concur/list_expected_expenses.ts @@ -0,0 +1,100 @@ +import type { ListExpectedExpensesParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listExpectedExpensesTool: ToolConfig< + ListExpectedExpensesParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_list_expected_expenses', + name: 'SAP Concur List Expected Expenses', + description: + 'List expected expenses on a travel request (GET /travelrequest/v4/requests/{requestUuid}/expenses).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + requestUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Travel request UUID', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User UUID acting on the request (optional)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const requestUuid = trimRequired(params.requestUuid, 'requestUuid') + const query: Record = {} + if (params.userId?.trim()) query.userId = params.userId.trim() + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests/${encodeURIComponent(requestUuid)}/expenses`, + method: 'GET', + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: + 'Array of expected expense objects. Each entry includes id, href, expenseType {id,name}, transactionDate, transactionAmount, postedAmount, approvedAmount, remainingAmount, businessPurpose, location, exchangeRate, allocations, tripData, parentRequest {href, id}, comments {href, id}.', + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_expense_reports.ts b/apps/sim/tools/sap_concur/list_expense_reports.ts new file mode 100644 index 00000000000..26e98db97ce --- /dev/null +++ b/apps/sim/tools/sap_concur/list_expense_reports.ts @@ -0,0 +1,275 @@ +import type { ListExpenseReportsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listExpenseReportsTool: ToolConfig = + { + id: 'sap_concur_list_expense_reports', + name: 'SAP Concur List Expense Reports', + description: + 'List expense reports (GET /api/v3.0/expense/reports). Returns a v3 envelope { Items, NextPage }.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Concur datacenter base URL (us, us2, eu, eu2, cn, emea — defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + user: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by a specific user (login id or user identifier).', + }, + submitDateBefore: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter to reports submitted on or before this date (YYYY-MM-DD)', + }, + submitDateAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter to reports submitted on or after this date (YYYY-MM-DD)', + }, + paidDateBefore: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter to reports paid on or before this date (YYYY-MM-DD)', + }, + paidDateAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter to reports paid on or after this date (YYYY-MM-DD)', + }, + modifiedDateBefore: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter to reports last modified on or before this date (YYYY-MM-DD)', + }, + modifiedDateAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter to reports last modified on or after this date (YYYY-MM-DD)', + }, + createDateBefore: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter to reports created on or before this date (YYYY-MM-DD)', + }, + createDateAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter to reports created on or after this date (YYYY-MM-DD)', + }, + approvalStatusCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by approval status code (e.g. A_NOTF, A_PEND, A_APPR)', + }, + paymentStatusCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by payment status code', + }, + currencyCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by ISO currency code (e.g. USD, EUR)', + }, + approverLoginID: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by approver login ID', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of records per page (default 25, max 100)', + }, + offset: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Opaque cursor token returned by a prior call (NextPage).', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const query: Record = {} + if (params.user) query.user = params.user + if (params.submitDateBefore) query.submitDateBefore = params.submitDateBefore + if (params.submitDateAfter) query.submitDateAfter = params.submitDateAfter + if (params.paidDateBefore) query.paidDateBefore = params.paidDateBefore + if (params.paidDateAfter) query.paidDateAfter = params.paidDateAfter + if (params.modifiedDateBefore) query.modifiedDateBefore = params.modifiedDateBefore + if (params.modifiedDateAfter) query.modifiedDateAfter = params.modifiedDateAfter + if (params.createDateBefore) query.createDateBefore = params.createDateBefore + if (params.createDateAfter) query.createDateAfter = params.createDateAfter + if (params.approvalStatusCode) query.approvalStatusCode = params.approvalStatusCode + if (params.paymentStatusCode) query.paymentStatusCode = params.paymentStatusCode + if (params.currencyCode) query.currencyCode = params.currencyCode + if (params.approverLoginID) query.approverLoginID = params.approverLoginID + if (params.limit !== undefined) query.limit = params.limit + if (params.offset) query.offset = params.offset + return { + ...baseProxyBody(params), + path: '/api/v3.0/expense/reports', + method: 'GET', + query, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Concur v3 expense reports envelope', + properties: { + Items: { + type: 'array', + description: 'Array of report header objects', + optional: true, + items: { + type: 'json', + properties: { + ID: { type: 'string', description: 'Report ID', optional: true }, + Name: { type: 'string', description: 'Report name', optional: true }, + OwnerLoginID: { type: 'string', description: 'Owner login ID', optional: true }, + OwnerName: { type: 'string', description: 'Owner display name', optional: true }, + Total: { type: 'number', description: 'Report total', optional: true }, + TotalApprovedAmount: { + type: 'number', + description: 'Total approved amount', + optional: true, + }, + TotalClaimedAmount: { + type: 'number', + description: 'Total claimed amount', + optional: true, + }, + AmountDueEmployee: { + type: 'number', + description: 'Amount due employee', + optional: true, + }, + CurrencyCode: { type: 'string', description: 'ISO currency code', optional: true }, + ApprovalStatusName: { + type: 'string', + description: 'Approval status name', + optional: true, + }, + ApprovalStatusCode: { + type: 'string', + description: 'Approval status code', + optional: true, + }, + PaymentStatusName: { + type: 'string', + description: 'Payment status name', + optional: true, + }, + PaymentStatusCode: { + type: 'string', + description: 'Payment status code', + optional: true, + }, + ApproverLoginID: { + type: 'string', + description: 'Approver login ID', + optional: true, + }, + ApproverName: { + type: 'string', + description: 'Approver display name', + optional: true, + }, + HasException: { + type: 'boolean', + description: 'Whether the report has any exception', + optional: true, + }, + ReceiptsReceived: { + type: 'boolean', + description: 'Whether paper receipts were received', + optional: true, + }, + CreateDate: { type: 'string', description: 'Creation date', optional: true }, + SubmitDate: { type: 'string', description: 'Submit date', optional: true }, + LastModifiedDate: { + type: 'string', + description: 'Last modified date', + optional: true, + }, + PaidDate: { type: 'string', description: 'Paid date', optional: true }, + URI: { type: 'string', description: 'Self URI', optional: true }, + }, + }, + }, + NextPage: { + type: 'string', + description: 'URI of the next page (use as offset cursor)', + optional: true, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/list_expenses.ts b/apps/sim/tools/sap_concur/list_expenses.ts new file mode 100644 index 00000000000..69ea8f1a161 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_expenses.ts @@ -0,0 +1,216 @@ +import type { ListExpensesParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listExpensesTool: ToolConfig = { + id: 'sap_concur_list_expenses', + name: 'SAP Concur List Expenses', + description: + 'List expenses on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context (TRAVELER per the v4 spec)', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/expenses`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'array', + description: 'Array of expense summary entries (ReportExpenseSummary[])', + items: { + type: 'json', + properties: { + expenseId: { type: 'string', description: 'Expense identifier', optional: true }, + expenseType: { + type: 'json', + description: 'Expense type {id, name, code, isDeleted}', + optional: true, + }, + transactionDate: { + type: 'string', + description: 'Transaction date (YYYY-MM-DD)', + optional: true, + }, + transactionAmount: { + type: 'json', + description: 'Transaction amount {currencyCode, value}', + optional: true, + }, + postedAmount: { type: 'json', description: 'Posted amount', optional: true }, + approvedAmount: { type: 'json', description: 'Approved amount', optional: true }, + claimedAmount: { type: 'json', description: 'Claimed amount', optional: true }, + approverAdjustedAmount: { + type: 'json', + description: 'Approver-adjusted amount', + optional: true, + }, + paymentType: { + type: 'json', + description: 'Payment type {id, name, code}', + optional: true, + }, + vendor: { type: 'json', description: 'Vendor info', optional: true }, + location: { type: 'json', description: 'Location info', optional: true }, + allocationState: { + type: 'string', + description: 'Allocation state', + optional: true, + }, + allocationSetId: { + type: 'string', + description: 'Allocation set identifier', + optional: true, + }, + attendeeCount: { type: 'number', description: 'Attendee count', optional: true }, + businessPurpose: { + type: 'string', + description: 'Business purpose', + optional: true, + }, + hasBlockingExceptions: { + type: 'boolean', + description: 'Has submission-blocking exceptions', + optional: true, + }, + hasExceptions: { + type: 'boolean', + description: 'Has exceptions', + optional: true, + }, + hasMissingReceiptDeclaration: { + type: 'boolean', + description: 'Has missing-receipt declaration', + optional: true, + }, + isAutoCreated: { type: 'boolean', description: 'Auto-created', optional: true }, + isPersonalExpense: { + type: 'boolean', + description: 'Personal-expense flag', + optional: true, + }, + isImageRequired: { + type: 'boolean', + description: 'Receipt image required', + optional: true, + }, + isPaperReceiptRequired: { + type: 'boolean', + description: 'Paper receipt required', + optional: true, + }, + imageCertificationStatus: { + type: 'string', + description: 'Receipt image certification status', + optional: true, + }, + receiptImageId: { + type: 'string', + description: 'Receipt image identifier', + optional: true, + }, + ereceiptImageId: { + type: 'string', + description: 'eReceipt image identifier', + optional: true, + }, + ticketNumber: { + type: 'string', + description: 'Ticket number', + optional: true, + }, + exchangeRate: { type: 'json', description: 'Exchange rate', optional: true }, + travelAllowance: { + type: 'json', + description: 'Travel allowance', + optional: true, + }, + expenseSourceIdentifiers: { + type: 'json', + description: 'Expense source identifiers', + optional: true, + }, + links: { type: 'array', description: 'HATEOAS links', optional: true }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_itineraries.ts b/apps/sim/tools/sap_concur/list_itineraries.ts new file mode 100644 index 00000000000..6b46028fb06 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_itineraries.ts @@ -0,0 +1,232 @@ +import type { ListItinerariesParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listItinerariesTool: ToolConfig = { + id: 'sap_concur_list_itineraries', + name: 'SAP Concur List Trips', + description: 'List travel trips/itineraries (GET /api/travel/trip/v1.1).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + startDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter trips starting on/after this date (YYYY-MM-DD)', + }, + endDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter trips ending on/before this date (YYYY-MM-DD)', + }, + bookingType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by booking type (air, car, hotel, rail, etc.)', + }, + useridType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User identifier type (login, xmlsyncid, uuid)', + }, + useridValue: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User identifier value (paired with useridType)', + }, + itemsPerPage: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Items per page', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: '1-based page number', + }, + includeMetadata: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include paging metadata in the response', + }, + includeCanceledTrips: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include canceled trips in the result set', + }, + createdAfterDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only trips created after this date (YYYY-MM-DD)', + }, + createdBeforeDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only trips created before this date (YYYY-MM-DD)', + }, + lastModifiedDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only trips modified on/after this date (YYYY-MM-DD)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const query = buildListQuery({ + startDate: params.startDate, + endDate: params.endDate, + bookingType: params.bookingType, + useridType: params.useridType, + useridValue: params.useridValue, + itemsPerPage: params.itemsPerPage, + page: params.page, + includeMetadata: params.includeMetadata, + includeCanceledTrips: params.includeCanceledTrips, + createdAfterDate: params.createdAfterDate, + createdBeforeDate: params.createdBeforeDate, + lastModifiedDate: params.lastModifiedDate, + }) + return { + ...baseProxyBody(params), + path: `/api/travel/trip/v1.1`, + method: 'GET', + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Trips list payload (Itinerary v1.1 ConnectResponse)', + properties: { + Metadata: { + type: 'json', + description: 'Paging metadata (when includeMetadata=true)', + optional: true, + properties: { + Paging: { + type: 'json', + description: 'Pagination details', + optional: true, + properties: { + TotalPages: { type: 'number', description: 'Total pages', optional: true }, + TotalItems: { type: 'number', description: 'Total items', optional: true }, + Page: { type: 'number', description: 'Current page', optional: true }, + ItemsPerPage: { type: 'number', description: 'Items per page', optional: true }, + PreviousPageURL: { + type: 'string', + description: 'Previous page URL', + optional: true, + }, + NextPageURL: { type: 'string', description: 'Next page URL', optional: true }, + }, + }, + }, + }, + ItineraryInfoList: { + type: 'array', + description: 'List of itinerary summary records', + optional: true, + items: { + type: 'json', + properties: { + ItinLocator: { + type: 'string', + description: 'Trip locator (trip ID)', + optional: true, + }, + ClientLocator: { type: 'string', description: 'Client trip locator', optional: true }, + ItinSourceName: { + type: 'string', + description: 'Booking source name', + optional: true, + }, + BookedVia: { type: 'string', description: 'Booking channel', optional: true }, + TripName: { type: 'string', description: 'Trip name', optional: true }, + Status: { type: 'string', description: 'Trip status', optional: true }, + Description: { type: 'string', description: 'Trip description', optional: true }, + StartDateUtc: { type: 'string', description: 'Start (UTC)', optional: true }, + EndDateUtc: { type: 'string', description: 'End (UTC)', optional: true }, + StartDateLocal: { type: 'string', description: 'Start (local)', optional: true }, + EndDateLocal: { type: 'string', description: 'End (local)', optional: true }, + DateCreatedUtc: { type: 'string', description: 'Created (UTC)', optional: true }, + DateModifiedUtc: { type: 'string', description: 'Modified (UTC)', optional: true }, + DateBookedLocal: { type: 'string', description: 'Booked (local)', optional: true }, + UserLoginId: { type: 'string', description: 'Trip owner login id', optional: true }, + BookedByFirstName: { + type: 'string', + description: 'Booker first name', + optional: true, + }, + BookedByLastName: { type: 'string', description: 'Booker last name', optional: true }, + IsPersonal: { type: 'boolean', description: 'Personal trip flag', optional: true }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_list_items.ts b/apps/sim/tools/sap_concur/list_list_items.ts new file mode 100644 index 00000000000..2bdf668e4b2 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_list_items.ts @@ -0,0 +1,220 @@ +import type { ListListItemsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listListItemsTool: ToolConfig = { + id: 'sap_concur_list_list_items', + name: 'SAP Concur List List Items', + description: + 'List the top-level items (children) for a custom list (GET /list/v4/lists/{listId}/children).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + listId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'List ID', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (1-based; page size is fixed at 100)', + }, + sortBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort field: value or shortCode', + }, + sortDirection: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort direction: asc or desc', + }, + hasChildren: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include only items that have children', + }, + isDeleted: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include deleted items', + }, + shortCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by short code', + }, + value: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by display value', + }, + shortCodeOrValue: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by short code OR value', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const listId = trimRequired(params.listId, 'listId') + return { + ...baseProxyBody(params), + path: `/list/v4/lists/${encodeURIComponent(listId)}/children`, + method: 'GET', + query: buildListQuery({ + page: params.page, + sortBy: params.sortBy, + sortDirection: params.sortDirection, + hasChildren: params.hasChildren, + isDeleted: params.isDeleted, + shortCode: params.shortCode, + value: params.value, + shortCodeOrValue: params.shortCodeOrValue, + }), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Paginated list items collection', + properties: { + content: { + type: 'array', + description: 'List items in the current page', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'List item UUID', optional: true }, + code: { + type: 'string', + description: 'Long code format for the item', + optional: true, + }, + shortCode: { type: 'string', description: 'Short code identifier', optional: true }, + value: { type: 'string', description: 'Display value of the item', optional: true }, + parentId: { + type: 'string', + description: 'Parent item UUID (omitted for first-level items)', + optional: true, + }, + level: { + type: 'number', + description: 'Hierarchy level (1 for root items)', + optional: true, + }, + isDeleted: { + type: 'boolean', + description: 'Deletion status across all containing lists', + optional: true, + }, + lists: { + type: 'array', + description: 'Lists containing this item', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'List UUID', optional: true }, + hasChildren: { + type: 'boolean', + description: 'Whether this item has children in the list', + optional: true, + }, + }, + }, + }, + }, + }, + }, + page: { + type: 'json', + description: 'Pagination metadata', + optional: true, + properties: { + number: { type: 'number', description: 'Current page number', optional: true }, + size: { type: 'number', description: 'Items per page', optional: true }, + totalElements: { type: 'number', description: 'Total item count', optional: true }, + totalPages: { type: 'number', description: 'Total page count', optional: true }, + }, + }, + links: { + type: 'array', + description: 'Navigation links (next, previous, first, last)', + optional: true, + items: { + type: 'json', + properties: { + rel: { type: 'string', description: 'Link relation', optional: true }, + href: { type: 'string', description: 'Link URL', optional: true }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_lists.ts b/apps/sim/tools/sap_concur/list_lists.ts new file mode 100644 index 00000000000..d69dba59a00 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_lists.ts @@ -0,0 +1,209 @@ +import type { ListListsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listListsTool: ToolConfig = { + id: 'sap_concur_list_lists', + name: 'SAP Concur List Lists', + description: 'List custom lists (GET /list/v4/lists).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (1-based; page size is fixed at 100)', + }, + sortBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort field: name, levelcount, or listcategory', + }, + sortDirection: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort direction: asc or desc', + }, + value: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by list name', + }, + categoryType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by category type (mapped to category.type query param)', + }, + isDeleted: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include deleted lists', + }, + levelCount: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Filter by number of levels', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + ...baseProxyBody(params), + path: `/list/v4/lists`, + method: 'GET', + query: buildListQuery({ + page: params.page, + sortBy: params.sortBy, + sortDirection: params.sortDirection, + value: params.value, + 'category.type': params.categoryType, + isDeleted: params.isDeleted, + levelCount: params.levelCount, + }), + }), + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Paginated lists collection', + properties: { + content: { + type: 'array', + description: 'Lists in the current page', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'List UUID', optional: true }, + value: { type: 'string', description: 'Name of the list', optional: true }, + levelCount: { + type: 'number', + description: 'Number of levels in the list', + optional: true, + }, + searchCriteria: { + type: 'string', + description: 'Search attribute (TEXT or CODE)', + optional: true, + }, + displayFormat: { + type: 'string', + description: 'Display order ((CODE) TEXT or TEXT (CODE))', + optional: true, + }, + category: { + type: 'json', + description: 'List category', + optional: true, + properties: { + id: { type: 'string', description: 'Category UUID', optional: true }, + type: { type: 'string', description: 'Category type', optional: true }, + }, + }, + isReadOnly: { + type: 'boolean', + description: 'Whether the list is read-only', + optional: true, + }, + isDeleted: { + type: 'boolean', + description: 'Whether the list has been deleted', + optional: true, + }, + managedBy: { + type: 'string', + description: 'Managing application or service identifier', + optional: true, + }, + externalThreshold: { + type: 'number', + description: 'Threshold from where the level starts being external', + optional: true, + }, + }, + }, + }, + page: { + type: 'json', + description: 'Pagination metadata', + optional: true, + properties: { + number: { type: 'number', description: 'Current page number', optional: true }, + size: { type: 'number', description: 'Items per page', optional: true }, + totalElements: { type: 'number', description: 'Total item count', optional: true }, + totalPages: { type: 'number', description: 'Total page count', optional: true }, + }, + }, + links: { + type: 'array', + description: 'Navigation links (next, previous, first, last)', + optional: true, + items: { + type: 'json', + properties: { + rel: { type: 'string', description: 'Link relation', optional: true }, + href: { type: 'string', description: 'Link URL', optional: true }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_receipts.ts b/apps/sim/tools/sap_concur/list_receipts.ts new file mode 100644 index 00000000000..09ed01b302e --- /dev/null +++ b/apps/sim/tools/sap_concur/list_receipts.ts @@ -0,0 +1,107 @@ +import type { ListReceiptsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listReceiptsTool: ToolConfig = { + id: 'sap_concur_list_receipts', + name: 'SAP Concur List Receipts', + description: 'List receipts for a user (GET /receipts/v4/users/{userId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + return { + ...baseProxyBody(params), + path: `/receipts/v4/users/${encodeURIComponent(userId)}`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'array', + description: 'Array of e-receipt objects', + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Receipt id', optional: true }, + userId: { type: 'string', description: 'Owner user UUID', optional: true }, + dateTimeReceived: { + type: 'string', + description: 'Timestamp the receipt was received', + optional: true, + }, + receipt: { type: 'json', description: 'Structured receipt data', optional: true }, + image: { type: 'string', description: 'Receipt image URL or reference', optional: true }, + validationSchema: { + type: 'string', + description: 'Validation schema URI', + optional: true, + }, + self: { type: 'string', description: 'Self URL', optional: true }, + template: { type: 'string', description: 'Template URL', optional: true }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_report_comments.ts b/apps/sim/tools/sap_concur/list_report_comments.ts new file mode 100644 index 00000000000..c5096de1d06 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_report_comments.ts @@ -0,0 +1,154 @@ +import type { ListReportCommentsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listReportCommentsTool: ToolConfig = + { + id: 'sap_concur_list_report_comments', + name: 'SAP Concur List Report Comments', + description: + 'List comments on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/comments).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: TRAVELER, MANAGER, or PROXY', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + includeAllComments: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include comments from all expenses in the report (default false)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/comments`, + method: 'GET', + query: buildListQuery({ includeAllComments: params.includeAllComments }), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'array', + description: 'Array of report comment entries', + items: { + type: 'json', + properties: { + comment: { type: 'string', description: 'Comment text' }, + creationDate: { + type: 'string', + description: 'Comment creation timestamp (ISO 8601)', + }, + expenseId: { type: 'string', description: 'Related expense entry ID' }, + isAuditorComment: { + type: 'boolean', + description: 'Whether the comment was added by an auditor', + }, + isLatest: { + type: 'boolean', + description: 'Whether this is the latest comment', + }, + createdForEmployeeId: { + type: 'string', + description: 'Employee ID the comment was created for', + }, + author: { + type: 'json', + description: 'Comment author', + properties: { + employeeId: { type: 'string', description: 'Employee identifier' }, + employeeUuid: { type: 'string', description: 'Employee UUID' }, + }, + }, + createdForEmployee: { + type: 'json', + description: 'Employee the comment was created for', + properties: { + employeeId: { type: 'string', description: 'Employee identifier' }, + employeeUuid: { type: 'string', description: 'Employee UUID' }, + }, + }, + stepInstanceId: { + type: 'string', + description: 'Workflow step instance identifier', + optional: true, + }, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/list_reports_to_approve.ts b/apps/sim/tools/sap_concur/list_reports_to_approve.ts new file mode 100644 index 00000000000..cfa030e9fe6 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_reports_to_approve.ts @@ -0,0 +1,175 @@ +import type { ListReportsToApproveParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listReportsToApproveTool: ToolConfig< + ListReportsToApproveParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_list_reports_to_approve', + name: 'SAP Concur List Reports To Approve', + description: + 'List expense reports awaiting approval (GET /expensereports/v4/users/{userId}/context/MANAGER/reportsToApprove).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Manager user UUID', + }, + contextType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Access context: must be MANAGER (default)', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Report field name to sort by (e.g., reportDate)', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort direction: asc or desc', + }, + includeDelegateApprovals: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to include reports the caller can approve as a delegate', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = (params.contextType ?? 'MANAGER').trim() || 'MANAGER' + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reportsToApprove`, + method: 'GET', + query: buildListQuery({ + sort: params.sort, + order: params.order, + includeDelegateApprovals: params.includeDelegateApprovals, + }), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'array', + description: 'Array of reports awaiting approval (ReportToApprove[])', + items: { + type: 'json', + properties: { + reportId: { type: 'string', description: 'Unique report identifier' }, + name: { type: 'string', description: 'Report name' }, + reportDate: { type: 'string', description: 'Report date (YYYY-MM-DD)', optional: true }, + reportNumber: { + type: 'string', + description: 'User-friendly report number', + optional: true, + }, + submitDate: { + type: 'string', + description: 'Submission timestamp (ISO 8601 UTC)', + optional: true, + }, + approver: { + type: 'json', + description: 'Approver employee { employeeId, employeeUuid }', + optional: true, + }, + employee: { + type: 'json', + description: 'Report owner employee { employeeId, employeeUuid }', + optional: true, + }, + amountDueEmployee: { + type: 'json', + description: 'Amount due employee { value, currencyCode }', + optional: true, + }, + claimedAmount: { + type: 'json', + description: 'Total claimed amount { value, currencyCode }', + optional: true, + }, + totalApprovedAmount: { + type: 'json', + description: 'Total approved amount { value, currencyCode }', + optional: true, + }, + hasExceptions: { + type: 'boolean', + description: 'Whether the report has exceptions', + optional: true, + }, + reportType: { + type: 'string', + description: 'Report creation method identifier', + optional: true, + }, + links: { type: 'array', description: 'HATEOAS links', optional: true }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_travel_profiles_summary.ts b/apps/sim/tools/sap_concur/list_travel_profiles_summary.ts new file mode 100644 index 00000000000..62a0ea05147 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_travel_profiles_summary.ts @@ -0,0 +1,208 @@ +import type { + ListTravelProfilesSummaryParams, + SapConcurProxyResponse, +} from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listTravelProfilesSummaryTool: ToolConfig< + ListTravelProfilesSummaryParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_list_travel_profiles_summary', + name: 'SAP Concur List Travel Profiles Summary', + description: + 'List travel profile summaries (GET /api/travelprofile/v2.0/summary). LastModifiedDate is required by Concur.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + lastModifiedDate: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Required ISO 8601 date (YYYY-MM-DD or full timestamp)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: '1-based page number', + }, + itemsPerPage: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Items per page (max 200)', + }, + active: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Status filter (sent as Status query param): "Active" or "Inactive". Omit for all.', + }, + travelConfigs: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated travel configuration ids', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const lastModifiedDate = trimRequired(params.lastModifiedDate, 'lastModifiedDate') + const query = buildListQuery({ + LastModifiedDate: lastModifiedDate, + Page: params.page, + ItemsPerPage: params.itemsPerPage, + Status: params.active, + travelConfigs: params.travelConfigs, + }) + return { + ...baseProxyBody(params), + path: '/api/travelprofile/v2.0/summary', + method: 'GET', + query, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Travel profile summary list payload (Concur returns XML mapped to JSON)', + properties: { + Metadata: { + type: 'json', + description: 'Paging metadata', + optional: true, + properties: { + Paging: { + type: 'json', + description: 'Pagination details', + optional: true, + properties: { + TotalPages: { + type: 'number', + description: 'Total number of pages', + optional: true, + }, + TotalItems: { + type: 'number', + description: 'Total number of items', + optional: true, + }, + Page: { + type: 'number', + description: 'Current page', + optional: true, + }, + ItemsPerPage: { + type: 'number', + description: 'Items per page', + optional: true, + }, + PreviousPageURL: { + type: 'string', + description: 'URL to the previous page', + optional: true, + }, + NextPageURL: { + type: 'string', + description: 'URL to the next page', + optional: true, + }, + }, + }, + }, + }, + Data: { + type: 'array', + description: 'Array of travel profile summaries', + optional: true, + items: { + type: 'json', + properties: { + Status: { type: 'string', description: 'Status (Active/Inactive)', optional: true }, + LoginID: { type: 'string', description: 'Login identifier', optional: true }, + XmlProfileSyncID: { + type: 'string', + description: 'XML profile sync identifier', + optional: true, + }, + ProfileLastModifiedUTC: { + type: 'string', + description: 'Last modified timestamp (UTC)', + optional: true, + }, + RuleClass: { + type: 'string', + description: 'Travel rule class assigned to the profile', + optional: true, + }, + TravelConfigID: { + type: 'string', + description: 'Travel configuration identifier', + optional: true, + }, + UUID: { type: 'string', description: 'Profile UUID', optional: true }, + EmployeeID: { type: 'string', description: 'Employee ID', optional: true }, + CompanyID: { type: 'string', description: 'Company ID', optional: true }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_travel_request_comments.ts b/apps/sim/tools/sap_concur/list_travel_request_comments.ts new file mode 100644 index 00000000000..157d65f46a0 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_travel_request_comments.ts @@ -0,0 +1,118 @@ +import type { + ListTravelRequestCommentsParams, + SapConcurProxyResponse, +} from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listTravelRequestCommentsTool: ToolConfig< + ListTravelRequestCommentsParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_list_travel_request_comments', + name: 'SAP Concur List Travel Request Comments', + description: + 'List comments on a travel request (GET /travelrequest/v4/requests/{requestUuid}/comments).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + requestUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Travel request UUID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const requestUuid = trimRequired(params.requestUuid, 'requestUuid') + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests/${encodeURIComponent(requestUuid)}/comments`, + method: 'GET', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'array', + description: 'Array of comment entries', + items: { + type: 'json', + properties: { + author: { + type: 'json', + description: 'Comment author', + optional: true, + properties: { + firstName: { type: 'string', description: 'Author first name', optional: true }, + lastName: { type: 'string', description: 'Author last name', optional: true }, + }, + }, + creationDateTime: { + type: 'string', + description: 'Comment creation timestamp (ISO 8601)', + optional: true, + }, + isLatest: { + type: 'boolean', + description: 'Whether this is the latest comment', + optional: true, + }, + value: { type: 'string', description: 'Comment text', optional: true }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_travel_requests.ts b/apps/sim/tools/sap_concur/list_travel_requests.ts new file mode 100644 index 00000000000..2f1facc29d5 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_travel_requests.ts @@ -0,0 +1,349 @@ +import type { ListTravelRequestsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listTravelRequestsTool: ToolConfig = + { + id: 'sap_concur_list_travel_requests', + name: 'SAP Concur List Travel Requests', + description: 'List travel requests (GET /travelrequest/v4/requests).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + view: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'View filter (e.g., ALL, ACTIVE, PENDING, TOAPPROVE)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Max number of results per page', + }, + start: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page start cursor (offset)', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by Concur user UUID', + }, + approvedBefore: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 8601 date — return requests approved before this date', + }, + approvedAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 8601 date — return requests approved after this date', + }, + modifiedBefore: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 8601 date — return requests modified before this date', + }, + modifiedAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 8601 date — return requests modified after this date', + }, + sortField: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Field to sort by: startDate, approvalStatus, or requestId (default startDate)', + }, + sortOrder: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: ASC or DESC (default DESC)', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + ...baseProxyBody(params), + path: `/travelrequest/v4/requests`, + method: 'GET', + query: buildListQuery({ + view: params.view, + limit: params.limit, + start: params.start, + userId: params.userId, + approvedBefore: params.approvedBefore, + approvedAfter: params.approvedAfter, + modifiedBefore: params.modifiedBefore, + modifiedAfter: params.modifiedAfter, + sortField: params.sortField, + sortOrder: params.sortOrder ? params.sortOrder.toUpperCase() : undefined, + }), + }), + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Travel requests list payload', + properties: { + data: { + type: 'array', + description: 'Array of travel request summaries', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Travel request UUID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + requestId: { + type: 'string', + description: 'Public-facing request ID', + optional: true, + }, + name: { type: 'string', description: 'Request name', optional: true }, + businessPurpose: { + type: 'string', + description: 'Business purpose', + optional: true, + }, + comment: { type: 'string', description: 'Last attached comment', optional: true }, + creationDate: { + type: 'string', + description: 'Creation timestamp', + optional: true, + }, + submitDate: { + type: 'string', + description: 'Last submission timestamp', + optional: true, + }, + startDate: { + type: 'string', + description: 'Trip start date (ISO 8601)', + optional: true, + }, + endDate: { + type: 'string', + description: 'Trip end date (ISO 8601)', + optional: true, + }, + startTime: { + type: 'string', + description: 'Trip start time (HH:mm)', + optional: true, + }, + approved: { + type: 'boolean', + description: 'Whether the request is approved', + optional: true, + }, + pendingApproval: { + type: 'boolean', + description: 'Pending approval flag', + optional: true, + }, + closed: { type: 'boolean', description: 'Closed flag', optional: true }, + everSentBack: { + type: 'boolean', + description: 'Ever-sent-back flag', + optional: true, + }, + canceledPostApproval: { + type: 'boolean', + description: 'Canceled after approval flag', + optional: true, + }, + approvalStatus: { + type: 'json', + description: 'Approval status', + optional: true, + properties: { + code: { + type: 'string', + description: + 'Status code (NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK)', + optional: true, + }, + name: { + type: 'string', + description: 'Localized status name', + optional: true, + }, + }, + }, + owner: { + type: 'json', + description: 'Travel request owner', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { + type: 'string', + description: 'Owner first name', + optional: true, + }, + lastName: { + type: 'string', + description: 'Owner last name', + optional: true, + }, + }, + }, + approver: { + type: 'json', + description: 'Approver assigned to the request', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { + type: 'string', + description: 'Approver first name', + optional: true, + }, + lastName: { + type: 'string', + description: 'Approver last name', + optional: true, + }, + }, + }, + type: { + type: 'json', + description: 'Request type', + optional: true, + properties: { + code: { type: 'string', description: 'Request type code', optional: true }, + label: { + type: 'string', + description: 'Request type label', + optional: true, + }, + }, + }, + totalApprovedAmount: { + type: 'json', + description: 'Total approved amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { + type: 'string', + description: 'Currency code', + optional: true, + }, + }, + }, + totalPostedAmount: { + type: 'json', + description: 'Total posted amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { + type: 'string', + description: 'Currency code', + optional: true, + }, + }, + }, + totalRemainingAmount: { + type: 'json', + description: 'Total remaining amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { + type: 'string', + description: 'Currency code', + optional: true, + }, + }, + }, + expenses: { + type: 'array', + description: 'Resource links to expected expenses', + optional: true, + items: { type: 'json' }, + }, + }, + }, + }, + operations: { + type: 'array', + description: 'Pagination links (next, prev, first, last)', + optional: true, + items: { + type: 'json', + properties: { + rel: { type: 'string', description: 'Link relation', optional: true }, + href: { type: 'string', description: 'Link target', optional: true }, + method: { type: 'string', description: 'HTTP method', optional: true }, + name: { type: 'string', description: 'Link name', optional: true }, + }, + }, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/list_users.ts b/apps/sim/tools/sap_concur/list_users.ts new file mode 100644 index 00000000000..364c8ab991f --- /dev/null +++ b/apps/sim/tools/sap_concur/list_users.ts @@ -0,0 +1,109 @@ +import type { ListUsersParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + scimListResponseOutputProperties, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listUsersTool: ToolConfig = { + id: 'sap_concur_list_users', + name: 'SAP Concur List Users', + description: 'List Concur user identities (GET /profile/identity/v4.1/Users).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + count: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Max number of users to return (default 100, max 1000)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'SCIM v4.1 pagination cursor returned by a prior call', + }, + attributes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of attributes to include in the response', + }, + excludedAttributes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of attributes to exclude from the response', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + ...baseProxyBody(params), + path: `/profile/identity/v4.1/Users`, + method: 'GET', + query: buildListQuery({ + count: params.count, + cursor: params.cursor, + attributes: params.attributes, + excludedAttributes: params.excludedAttributes, + }), + }), + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'SCIM ListResponse with Resources array', + properties: scimListResponseOutputProperties, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/move_travel_request.ts b/apps/sim/tools/sap_concur/move_travel_request.ts new file mode 100644 index 00000000000..0e8ed0016c4 --- /dev/null +++ b/apps/sim/tools/sap_concur/move_travel_request.ts @@ -0,0 +1,151 @@ +import type { MoveTravelRequestParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const moveTravelRequestTool: ToolConfig = { + id: 'sap_concur_move_travel_request', + name: 'SAP Concur Move Travel Request', + description: + 'Move a travel request through workflow (POST /travelrequest/v4/requests/{requestUuid}/{action}). Valid actions: submit, recall, cancel, approve, sendback, close, reopen.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + requestUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Travel request UUID', + }, + action: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Workflow action: submit, recall, cancel, approve, sendback, close, reopen', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional Concur user UUID — required when impersonating another user', + }, + body: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Optional payload (e.g., { "comment": "..." })', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const requestUuid = trimRequired(params.requestUuid, 'requestUuid') + const action = trimRequired(params.action, 'action') + const query: Record = {} + if (params.userId) query.userId = params.userId + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests/${encodeURIComponent(requestUuid)}/${encodeURIComponent(action)}`, + method: 'POST', + body: params.body ?? {}, + query: Object.keys(query).length > 0 ? query : undefined, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Workflow transition response payload', + properties: { + id: { type: 'string', description: 'Travel request UUID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + approvalStatus: { + type: 'json', + description: 'Approval status after the workflow transition', + optional: true, + properties: { + code: { + type: 'string', + description: 'Status code (NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK)', + optional: true, + }, + name: { type: 'string', description: 'Localized status name', optional: true }, + }, + }, + approver: { + type: 'json', + description: 'Approver assigned after the transition', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { type: 'string', description: 'Approver first name', optional: true }, + lastName: { type: 'string', description: 'Approver last name', optional: true }, + }, + }, + operations: { + type: 'array', + description: 'Available follow-up workflow actions', + optional: true, + items: { + type: 'json', + properties: { + rel: { type: 'string', description: 'Link relation', optional: true }, + href: { type: 'string', description: 'Link target', optional: true }, + method: { type: 'string', description: 'HTTP method', optional: true }, + name: { type: 'string', description: 'Link name', optional: true }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/recall_expense_report.ts b/apps/sim/tools/sap_concur/recall_expense_report.ts new file mode 100644 index 00000000000..2b44390c2d9 --- /dev/null +++ b/apps/sim/tools/sap_concur/recall_expense_report.ts @@ -0,0 +1,109 @@ +import type { RecallExpenseReportParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const recallExpenseReportTool: ToolConfig< + RecallExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_recall_expense_report', + name: 'SAP Concur Recall Expense Report', + description: + 'Recall a submitted expense report (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/recall — supported contexts: TRAVELER, PROXY). No request body is required.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID who owns the report', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: TRAVELER or PROXY', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID to recall', + }, + body: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + "Optional body. Concur docs don't define a payload for this action; pass an empty object if uncertain.", + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/recall`, + method: 'PATCH', + body: params.body ?? {}, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { type: 'json', description: 'Empty (204 No Content)' }, + }, +} diff --git a/apps/sim/tools/sap_concur/remove_all_attendees.ts b/apps/sim/tools/sap_concur/remove_all_attendees.ts new file mode 100644 index 00000000000..94d53f8c7d7 --- /dev/null +++ b/apps/sim/tools/sap_concur/remove_all_attendees.ts @@ -0,0 +1,110 @@ +import type { RemoveAllAttendeesParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const removeAllAttendeesTool: ToolConfig = + { + id: 'sap_concur_remove_all_attendees', + name: 'SAP Concur Remove All Attendees', + description: + 'Remove all attendees from an expense (DELETE /expensereports/v4/users/{userId}/context/TRAVELER/reports/{reportId}/expenses/{expenseId}/attendees).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: TRAVELER or PROXY', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + expenseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + const expenseId = trimRequired(params.expenseId, 'expenseId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/expenses/${encodeURIComponent(expenseId)}/attendees`, + method: 'DELETE', + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Empty response body (Concur returns 204 No Content)', + properties: {}, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/search_locations.ts b/apps/sim/tools/sap_concur/search_locations.ts new file mode 100644 index 00000000000..5acfbe35187 --- /dev/null +++ b/apps/sim/tools/sap_concur/search_locations.ts @@ -0,0 +1,227 @@ +import type { SapConcurProxyResponse, SearchLocationsParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const searchLocationsTool: ToolConfig = { + id: 'sap_concur_search_locations', + name: 'SAP Concur Search Locations', + description: 'Search Concur location reference data (GET /localities/v5/locations).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + searchText: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Free-text query (city, airport, landmark, etc.)', + }, + locCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'IATA / location code', + }, + locationNameId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Concur internal location name ID (UUID)', + }, + locationNameKey: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Concur internal numeric location name key', + }, + countryCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: '2-letter ISO 3166-1 country code', + }, + subdivisionCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 3166-2:2007 country subdivision (e.g. US-WA)', + }, + adminRegionId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Administrative region ID', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const query: Record = {} + if (params.searchText) query.searchText = params.searchText + if (params.locCode) query.locCode = params.locCode + if (params.locationNameId) query.locationNameId = params.locationNameId + if (params.locationNameKey !== undefined && params.locationNameKey !== null) + query.locationNameKey = params.locationNameKey + if ( + query.searchText === undefined && + query.locCode === undefined && + query.locationNameId === undefined && + query.locationNameKey === undefined + ) { + throw new Error( + 'search_locations requires at least one of: searchText, locCode, locationNameId, locationNameKey' + ) + } + if (params.countryCode) query.countryCode = params.countryCode + if (params.subdivisionCode) query.subdivisionCode = params.subdivisionCode + if (params.adminRegionId) query.adminRegionId = params.adminRegionId + return { + ...baseProxyBody(params), + path: '/localities/v5/locations', + method: 'GET', + query, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Localities v5 search response', + properties: { + locations: { + type: 'array', + description: 'Array of matching Location records', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Location ID (UUID)', optional: true }, + code: { type: 'string', description: 'IATA / location code', optional: true }, + legacyKey: { + type: 'number', + description: 'Legacy numeric location key', + optional: true, + }, + timeZoneOffset: { + type: 'string', + description: 'IANA timezone or UTC offset', + optional: true, + }, + active: { + type: 'boolean', + description: 'Whether the location is active', + optional: true, + }, + point: { + type: 'json', + description: 'Geographic coordinates', + optional: true, + properties: { + latitude: { type: 'number', description: 'Latitude', optional: true }, + longitude: { type: 'number', description: 'Longitude', optional: true }, + }, + }, + names: { + type: 'array', + description: 'Localized location names', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Name ID', optional: true }, + key: { type: 'number', description: 'Numeric name key', optional: true }, + locale: { type: 'string', description: 'Locale tag', optional: true }, + name: { type: 'string', description: 'Display name', optional: true }, + }, + }, + }, + administrativeRegion: { + type: 'json', + description: 'Administrative region (e.g., metro area)', + optional: true, + properties: { + id: { type: 'string', description: 'Region ID', optional: true }, + name: { type: 'string', description: 'Region name', optional: true }, + }, + }, + country: { + type: 'json', + description: 'Country reference', + optional: true, + properties: { + id: { type: 'string', description: 'Country ID', optional: true }, + code: { type: 'string', description: 'ISO country code', optional: true }, + name: { type: 'string', description: 'Country name', optional: true }, + }, + }, + subDivision: { + type: 'json', + description: 'Country subdivision (state/province)', + optional: true, + properties: { + id: { type: 'string', description: 'Subdivision ID', optional: true }, + code: { type: 'string', description: 'ISO subdivision code', optional: true }, + name: { type: 'string', description: 'Subdivision name', optional: true }, + }, + }, + links: { + type: 'array', + description: 'HATEOAS links', + optional: true, + items: { type: 'json' }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/search_users.ts b/apps/sim/tools/sap_concur/search_users.ts new file mode 100644 index 00000000000..73fb6bd94eb --- /dev/null +++ b/apps/sim/tools/sap_concur/search_users.ts @@ -0,0 +1,87 @@ +import type { SapConcurProxyResponse, SearchUsersParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + scimListResponseOutputProperties, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const searchUsersTool: ToolConfig = { + id: 'sap_concur_search_users', + name: 'SAP Concur Search Users', + description: + 'Search users via SCIM .search endpoint (POST /profile/identity/v4.1/Users/.search).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'SCIM search request payload ({ schemas, attributes, filter, count, startIndex })', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + ...baseProxyBody(params), + path: `/profile/identity/v4.1/Users/.search`, + method: 'POST', + body: params.body, + }), + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'SCIM search ListResponse', + properties: scimListResponseOutputProperties, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/send_back_expense_report.ts b/apps/sim/tools/sap_concur/send_back_expense_report.ts new file mode 100644 index 00000000000..85adfa1b72b --- /dev/null +++ b/apps/sim/tools/sap_concur/send_back_expense_report.ts @@ -0,0 +1,95 @@ +import type { SapConcurProxyResponse, SendBackExpenseReportParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const sendBackExpenseReportTool: ToolConfig< + SendBackExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_send_back_expense_report', + name: 'SAP Concur Send Back Expense Report', + description: + 'Send back an expense report to the employee (PATCH /expensereports/v4/reports/{reportId}/sendBack). Required body field: comment.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID to send back', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Request body — `comment` is required by Concur (e.g., { "comment": "Missing receipt" }). Optional fields: `expectedStepCode`, `expectedStepSequence`.', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const reportId = trimRequired(params.reportId, 'reportId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/reports/${encodeURIComponent(reportId)}/sendBack`, + method: 'PATCH', + body: params.body, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { type: 'json', description: 'Empty (204 No Content)' }, + }, +} diff --git a/apps/sim/tools/sap_concur/submit_expense_report.ts b/apps/sim/tools/sap_concur/submit_expense_report.ts new file mode 100644 index 00000000000..8dc30d7f5d6 --- /dev/null +++ b/apps/sim/tools/sap_concur/submit_expense_report.ts @@ -0,0 +1,102 @@ +import type { SapConcurProxyResponse, SubmitExpenseReportParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const submitExpenseReportTool: ToolConfig< + SubmitExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_submit_expense_report', + name: 'SAP Concur Submit Expense Report', + description: + 'Submit an expense report into the workflow via Expense Report v4 (PATCH /expensereports/v4/users/{userId}/reports/{reportId}/submit).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID who owns the report', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID to submit', + }, + body: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + "Optional body. Concur docs don't define a payload for this action; pass an empty object if uncertain.", + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const reportId = trimRequired(params.reportId, 'reportId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/reports/${encodeURIComponent(reportId)}/submit`, + method: 'PATCH', + body: params.body ?? {}, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { type: 'json', description: 'Empty (204 No Content)' }, + }, +} diff --git a/apps/sim/tools/sap_concur/types.ts b/apps/sim/tools/sap_concur/types.ts new file mode 100644 index 00000000000..570ad00c858 --- /dev/null +++ b/apps/sim/tools/sap_concur/types.ts @@ -0,0 +1,531 @@ +import type { ToolResponse } from '@/tools/types' + +export type SapConcurDatacenter = + | 'us.api.concursolutions.com' + | 'us2.api.concursolutions.com' + | 'eu.api.concursolutions.com' + | 'eu2.api.concursolutions.com' + | 'cn.api.concursolutions.com' + | 'emea.api.concursolutions.com' + +export type SapConcurGrantType = 'client_credentials' | 'password' | 'refresh_token' + +export interface SapConcurBaseParams { + datacenter?: SapConcurDatacenter + grantType?: SapConcurGrantType + clientId: string + clientSecret: string + username?: string + password?: string + companyUuid?: string +} + +export interface ProxyOutput { + status: number + data: unknown +} + +export interface SapConcurProxyResponse extends ToolResponse { + output: ProxyOutput +} + +export interface ListExpenseReportsParams extends SapConcurBaseParams { + user?: string + submitDateBefore?: string + submitDateAfter?: string + paidDateBefore?: string + paidDateAfter?: string + modifiedDateBefore?: string + modifiedDateAfter?: string + createDateBefore?: string + createDateAfter?: string + approvalStatusCode?: string + paymentStatusCode?: string + currencyCode?: string + approverLoginID?: string + limit?: number + offset?: string +} + +export interface GetExpenseReportParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' | 'PROCESSOR' | 'PROXY' + reportId: string +} + +export interface ListReportsToApproveParams extends SapConcurBaseParams { + userId: string + contextType?: 'MANAGER' + sort?: string + order?: string + includeDelegateApprovals?: boolean +} + +export interface CreateExpenseReportParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + body: Record | string +} + +export interface UpdateExpenseReportParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + reportId: string + body: Record | string +} + +export interface DeleteExpenseReportParams extends SapConcurBaseParams { + reportId: string +} + +export interface SubmitExpenseReportParams extends SapConcurBaseParams { + userId: string + reportId: string + body?: Record | string +} + +export interface RecallExpenseReportParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + reportId: string + body?: Record | string +} + +export interface ApproveExpenseReportParams extends SapConcurBaseParams { + reportId: string + body: Record | string +} + +export interface SendBackExpenseReportParams extends SapConcurBaseParams { + reportId: string + body: Record | string +} + +export interface ListExpensesParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' + reportId: string +} + +export interface GetExpenseParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' | 'PROXY' + reportId: string + expenseId: string +} + +export interface GetItemizationsParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' + reportId: string + expenseId: string +} + +export interface UpdateExpenseParams extends SapConcurBaseParams { + reportId: string + expenseId: string + body: Record | string +} + +export interface DeleteExpenseParams extends SapConcurBaseParams { + reportId: string + expenseId: string +} + +export interface ListAllocationsParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' | 'PROXY' + reportId: string + expenseId: string +} + +export interface GetAllocationParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' | 'PROXY' + reportId: string + allocationId: string +} + +export interface UpdateAllocationParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + reportId: string + allocationId: string + body: Record | string +} + +export interface ListAttendeeAssociationsParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + reportId: string + expenseId: string +} + +export interface AssociateAttendeesParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + reportId: string + expenseId: string + body: Record | string +} + +export interface RemoveAllAttendeesParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + reportId: string + expenseId: string +} + +export interface ListReportCommentsParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' | 'PROXY' + reportId: string + includeAllComments?: boolean +} + +export interface CreateReportCommentParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' | 'PROXY' + reportId: string + comment: string +} + +export interface ListExceptionsParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' | 'PROXY' + reportId: string +} + +export interface CreateQuickExpenseParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' + body: Record | string +} + +export interface ListReceiptsParams extends SapConcurBaseParams { + userId: string +} + +export interface GetReceiptParams extends SapConcurBaseParams { + receiptId: string +} + +export interface GetReceiptStatusParams extends SapConcurBaseParams { + receiptId: string +} + +export interface ListTravelRequestsParams extends SapConcurBaseParams { + view?: string + limit?: number + start?: number + userId?: string + approvedBefore?: string + approvedAfter?: string + modifiedBefore?: string + modifiedAfter?: string + sortField?: string + sortOrder?: 'asc' | 'desc' +} + +export interface GetTravelRequestParams extends SapConcurBaseParams { + requestUuid: string + userId?: string +} + +export interface CreateTravelRequestParams extends SapConcurBaseParams { + userId?: string + body: Record | string +} + +export interface UpdateTravelRequestParams extends SapConcurBaseParams { + requestUuid: string + body: Record | string +} + +export interface DeleteTravelRequestParams extends SapConcurBaseParams { + requestUuid: string + userId?: string +} + +export interface MoveTravelRequestParams extends SapConcurBaseParams { + requestUuid: string + action: string + userId?: string + body?: Record | string +} + +export interface ListTravelRequestCommentsParams extends SapConcurBaseParams { + requestUuid: string +} + +export interface GetRequestCashAdvanceParams extends SapConcurBaseParams { + cashAdvanceUuid: string +} + +export interface CreateExpectedExpenseParams extends SapConcurBaseParams { + requestUuid: string + userId?: string + body: Record | string +} + +export interface ListExpectedExpensesParams extends SapConcurBaseParams { + requestUuid: string + userId?: string +} + +export interface GetExpectedExpenseParams extends SapConcurBaseParams { + expenseUuid: string + userId?: string +} + +export interface UpdateExpectedExpenseParams extends SapConcurBaseParams { + expenseUuid: string + userId?: string + body: Record | string +} + +export interface DeleteExpectedExpenseParams extends SapConcurBaseParams { + expenseUuid: string + userId?: string +} + +export interface GetCashAdvanceParams extends SapConcurBaseParams { + cashAdvanceId: string +} + +export interface CreateCashAdvanceParams extends SapConcurBaseParams { + body: Record | string +} + +export interface IssueCashAdvanceParams extends SapConcurBaseParams { + cashAdvanceId: string + body?: Record | string +} + +export interface ListItinerariesParams extends SapConcurBaseParams { + startDate?: string + endDate?: string + bookingType?: string + useridType?: string + useridValue?: string + itemsPerPage?: number + page?: number + includeMetadata?: boolean + includeCanceledTrips?: boolean + createdAfterDate?: string + createdBeforeDate?: string + lastModifiedDate?: string +} + +export interface GetItineraryParams extends SapConcurBaseParams { + tripId: string + useridType?: string + useridValue?: string + systemFormat?: string +} + +export interface ListUsersParams extends SapConcurBaseParams { + count?: number + cursor?: string + attributes?: string + excludedAttributes?: string +} + +export interface SearchUsersParams extends SapConcurBaseParams { + body: Record | string +} + +export interface GetUserParams extends SapConcurBaseParams { + userUuid: string + attributes?: string + excludedAttributes?: string +} + +export interface CreateUserParams extends SapConcurBaseParams { + body: Record | string +} + +export interface UpdateUserParams extends SapConcurBaseParams { + userUuid: string + body: Record | string +} + +export interface DeleteUserParams extends SapConcurBaseParams { + userUuid: string +} + +export interface GetTravelProfileParams extends SapConcurBaseParams { + loginId?: string + userId?: string + useridType?: 'login' | 'xmlsyncid' | 'uuid' + useridValue?: string +} + +export interface ListTravelProfilesSummaryParams extends SapConcurBaseParams { + lastModifiedDate: string + page?: number + itemsPerPage?: number + active?: 'Active' | 'Inactive' + travelConfigs?: string +} + +export interface SearchLocationsParams extends SapConcurBaseParams { + searchText?: string + locCode?: string + locationNameId?: string + locationNameKey?: number + countryCode?: string + subdivisionCode?: string + adminRegionId?: string +} + +export interface CreateListItemParams extends SapConcurBaseParams { + body: Record | string +} + +export interface UpdateListItemParams extends SapConcurBaseParams { + itemId: string + body: Record | string +} + +export interface DeleteListItemParams extends SapConcurBaseParams { + itemId: string +} + +export interface UserFileLike { + id?: string + key?: string + path?: string + url?: string + name: string + size: number + type?: string + [key: string]: unknown +} + +export interface UploadReceiptImageParams extends SapConcurBaseParams { + userId: string + receipt: UserFileLike + forwardId?: string +} + +export interface CreateQuickExpenseWithImageParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' + receipt: UserFileLike + body: Record | string +} + +export interface ListInvoicesParams extends SapConcurBaseParams { + limit?: number + offset?: number + modifiedAfter?: string +} + +export interface GetInvoiceParams extends SapConcurBaseParams { + invoiceId: string +} + +export interface ListPurchaseOrdersParams extends SapConcurBaseParams { + limit?: number + offset?: number +} + +export interface GetPurchaseOrderParams extends SapConcurBaseParams { + purchaseOrderId: string +} + +export interface ListVendorsParams extends SapConcurBaseParams { + limit?: number + offset?: number + vendorCode?: string +} + +export interface ListPurchaseRequestsParams extends SapConcurBaseParams { + limit?: number + offset?: number + modifiedAfter?: string +} + +export interface GetPurchaseRequestParams extends SapConcurBaseParams { + purchaseRequestId: string +} + +export interface CreatePurchaseRequestParams extends SapConcurBaseParams { + body: Record | string +} + +export interface UpdatePurchaseRequestParams extends SapConcurBaseParams { + purchaseRequestId: string + body: Record | string +} + +export interface ListListsParams extends SapConcurBaseParams { + page?: number + sortBy?: string + sortDirection?: 'asc' | 'desc' + value?: string + categoryType?: string + isDeleted?: boolean + levelCount?: number +} + +export interface GetListParams extends SapConcurBaseParams { + listId: string +} + +export interface ListListItemsParams extends SapConcurBaseParams { + listId: string + page?: number + sortBy?: 'value' | 'shortCode' + sortDirection?: 'asc' | 'desc' + hasChildren?: boolean + isDeleted?: boolean + shortCode?: string + value?: string + shortCodeOrValue?: string +} + +export interface GetListItemParams extends SapConcurBaseParams { + itemId: string +} + +export interface ListBudgetsParams extends SapConcurBaseParams { + adminView?: boolean + offset?: number + responseSchema?: 'COMPACT' +} + +export interface GetBudgetParams extends SapConcurBaseParams { + budgetId: string +} + +export interface ListBudgetItemsParams extends SapConcurBaseParams { + budgetId: string + limit?: number + offset?: number +} + +export type ListBudgetCategoriesParams = SapConcurBaseParams + +export interface ListCardTransactionsParams extends SapConcurBaseParams { + limit?: number + offset?: number + cardAccountId?: string + user?: string + modifiedAfter?: string +} + +export interface GetCardTransactionParams extends SapConcurBaseParams { + cardTransactionId: string +} + +export interface GetExchangeRateParams extends SapConcurBaseParams { + body: Record | string +} + +export interface ListLocalitiesParams extends SapConcurBaseParams { + limit?: number + offset?: number + countryCode?: string +} diff --git a/apps/sim/tools/sap_concur/update_allocation.ts b/apps/sim/tools/sap_concur/update_allocation.ts new file mode 100644 index 00000000000..d5dfeabad74 --- /dev/null +++ b/apps/sim/tools/sap_concur/update_allocation.ts @@ -0,0 +1,116 @@ +import type { SapConcurProxyResponse, UpdateAllocationParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateAllocationTool: ToolConfig = { + id: 'sap_concur_update_allocation', + name: 'SAP Concur Update Allocation', + description: + 'Update an allocation (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/allocations/{allocationId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access context: TRAVELER or PROXY (write requires expense.report.readwrite)', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + allocationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Allocation ID to update', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Fields to update on the allocation', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + const allocationId = trimRequired(params.allocationId, 'allocationId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}/allocations/${encodeURIComponent(allocationId)}`, + method: 'PATCH', + body: params.body, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Empty body on success (Concur returns 204 No Content)', + properties: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/update_expected_expense.ts b/apps/sim/tools/sap_concur/update_expected_expense.ts new file mode 100644 index 00000000000..5d3c7a757e2 --- /dev/null +++ b/apps/sim/tools/sap_concur/update_expected_expense.ts @@ -0,0 +1,177 @@ +import type { SapConcurProxyResponse, UpdateExpectedExpenseParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateExpectedExpenseTool: ToolConfig< + UpdateExpectedExpenseParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_update_expected_expense', + name: 'SAP Concur Update Expected Expense', + description: 'Update an expected expense (PUT /travelrequest/v4/expenses/{expenseUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + expenseUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expected expense UUID to update', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'User UUID acting on the request (required when using a Company JWT, optional otherwise)', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Fields to update on the expected expense', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const expenseUuid = trimRequired(params.expenseUuid, 'expenseUuid') + const query: Record = {} + if (params.userId?.trim()) query.userId = params.userId.trim() + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/expenses/${encodeURIComponent(expenseUuid)}`, + method: 'PUT', + body: params.body, + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Updated expected expense payload', + properties: { + id: { type: 'string', description: 'Expected expense identifier', optional: true }, + href: { type: 'string', description: 'Self-link', optional: true }, + expenseType: { + type: 'json', + description: 'Expense type {id, name}', + optional: true, + }, + transactionDate: { + type: 'string', + description: 'Transaction date', + optional: true, + }, + transactionAmount: { + type: 'json', + description: 'Transaction amount {value, currencyCode}', + optional: true, + }, + postedAmount: { + type: 'json', + description: 'Posted amount {value, currencyCode}', + optional: true, + }, + approvedAmount: { + type: 'json', + description: 'Approved amount {value, currencyCode}', + optional: true, + }, + remainingAmount: { + type: 'json', + description: 'Remaining amount on the expected expense', + optional: true, + }, + businessPurpose: { + type: 'string', + description: 'Business purpose of the expense', + optional: true, + }, + location: { + type: 'json', + description: + 'Location {id, name, city, countryCode, countrySubDivisionCode, iataCode, locationType}', + optional: true, + }, + exchangeRate: { + type: 'json', + description: 'Exchange rate {value, operation}', + optional: true, + }, + allocations: { + type: 'json', + description: 'Budget allocations array', + optional: true, + }, + tripData: { + type: 'json', + description: + 'Trip data {agencyBooked, selfBooked, tripType (ONE_WAY|ROUND_TRIP), legs[{id, returnLeg, startDate, startTime, startLocationDetail, startLocation, endLocation, class {code,value}, travelExceptionReasonCodes}], segmentType {category, code}}', + optional: true, + }, + parentRequest: { + type: 'json', + description: 'Parent travel request resource link {href, id}', + optional: true, + }, + comments: { + type: 'json', + description: 'Comments sub-resource link {href, id}', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/update_expense.ts b/apps/sim/tools/sap_concur/update_expense.ts new file mode 100644 index 00000000000..2729e5b4d04 --- /dev/null +++ b/apps/sim/tools/sap_concur/update_expense.ts @@ -0,0 +1,104 @@ +import type { SapConcurProxyResponse, UpdateExpenseParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateExpenseTool: ToolConfig = { + id: 'sap_concur_update_expense', + name: 'SAP Concur Update Expense', + description: + 'Update an expense (PATCH /expensereports/v4/reports/{reportId}/expenses/{expenseId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID', + }, + expenseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense ID to update', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'PATCH body. Allowed fields: businessPurpose (string, max 64), customData (CustomData[]), expenseSource (required: EA|MOB|OTHER|SE|TA|TR|UI), isExpenseRejected (boolean), isPaperReceiptReceived (boolean).', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const reportId = trimRequired(params.reportId, 'reportId') + const expenseId = trimRequired(params.expenseId, 'expenseId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/reports/${encodeURIComponent(reportId)}/expenses/${encodeURIComponent(expenseId)}`, + method: 'PATCH', + body: params.body, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: + 'Empty body on success (HTTP 204 No Content). Error details when status is non-2xx', + properties: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/update_expense_report.ts b/apps/sim/tools/sap_concur/update_expense_report.ts new file mode 100644 index 00000000000..6e124d3b5c9 --- /dev/null +++ b/apps/sim/tools/sap_concur/update_expense_report.ts @@ -0,0 +1,109 @@ +import type { SapConcurProxyResponse, UpdateExpenseReportParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateExpenseReportTool: ToolConfig< + UpdateExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_update_expense_report', + name: 'SAP Concur Update Expense Report', + description: + 'Update an unsubmitted expense report (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId} — supported contexts: TRAVELER, PROXY). Body fields: businessPurpose, comment, customData, name, etc.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID who owns the report', + }, + contextType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Access context: TRAVELER (own report) or PROXY (editing on behalf of another user)', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Expense report ID to update', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Fields to update on the report', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + const contextType = trimRequired(params.contextType, 'contextType') + const reportId = trimRequired(params.reportId, 'reportId') + return { + ...baseProxyBody(params), + path: `/expensereports/v4/users/${encodeURIComponent(userId)}/context/${encodeURIComponent(contextType)}/reports/${encodeURIComponent(reportId)}`, + method: 'PATCH', + body: params.body, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { type: 'json', description: 'Empty (204 No Content)' }, + }, +} diff --git a/apps/sim/tools/sap_concur/update_list_item.ts b/apps/sim/tools/sap_concur/update_list_item.ts new file mode 100644 index 00000000000..df8a9ac0de5 --- /dev/null +++ b/apps/sim/tools/sap_concur/update_list_item.ts @@ -0,0 +1,131 @@ +import type { SapConcurProxyResponse, UpdateListItemParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateListItemTool: ToolConfig = { + id: 'sap_concur_update_list_item', + name: 'SAP Concur Update List Item', + description: 'Update a list item (PUT /list/v4/items/{itemId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'List item UUID', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'List item payload. Required: shortCode, value. Other fields in the body are ignored.', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const itemId = trimRequired(params.itemId, 'itemId') + return { + ...baseProxyBody(params), + path: `/list/v4/items/${encodeURIComponent(itemId)}`, + method: 'PUT', + body: params.body, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Updated list item', + properties: { + id: { type: 'string', description: 'List item UUID', optional: true }, + code: { type: 'string', description: 'Long code format for the item', optional: true }, + shortCode: { type: 'string', description: 'Short code identifier', optional: true }, + value: { type: 'string', description: 'Display value of the item', optional: true }, + parentId: { + type: 'string', + description: 'Parent item UUID (omitted for first-level items)', + optional: true, + }, + level: { + type: 'number', + description: 'Hierarchy level (1 for root items)', + optional: true, + }, + isDeleted: { + type: 'boolean', + description: 'Deletion status across all containing lists', + optional: true, + }, + lists: { + type: 'array', + description: 'Lists containing this item', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'List UUID', optional: true }, + hasChildren: { + type: 'boolean', + description: 'Whether this item has children in the list', + optional: true, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/update_travel_request.ts b/apps/sim/tools/sap_concur/update_travel_request.ts new file mode 100644 index 00000000000..405a362ca91 --- /dev/null +++ b/apps/sim/tools/sap_concur/update_travel_request.ts @@ -0,0 +1,231 @@ +import type { SapConcurProxyResponse, UpdateTravelRequestParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateTravelRequestTool: ToolConfig< + UpdateTravelRequestParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_update_travel_request', + name: 'SAP Concur Update Travel Request', + description: 'Update a travel request (PUT /travelrequest/v4/requests/{requestUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + requestUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Travel request UUID to update', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Fields to update on the travel request', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const requestUuid = trimRequired(params.requestUuid, 'requestUuid') + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests/${encodeURIComponent(requestUuid)}`, + method: 'PUT', + body: params.body, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Updated travel request payload', + properties: { + id: { type: 'string', description: 'Travel request UUID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + requestId: { + type: 'string', + description: 'Public-facing request ID (4-6 alphanumeric characters)', + optional: true, + }, + name: { type: 'string', description: 'Request name', optional: true }, + businessPurpose: { type: 'string', description: 'Business purpose', optional: true }, + comment: { type: 'string', description: 'Last attached comment', optional: true }, + creationDate: { type: 'string', description: 'Creation timestamp', optional: true }, + lastModified: { + type: 'string', + description: 'Last modification timestamp', + optional: true, + }, + submitDate: { type: 'string', description: 'Last submission timestamp', optional: true }, + startDate: { type: 'string', description: 'Trip start date (ISO 8601)', optional: true }, + endDate: { type: 'string', description: 'Trip end date (ISO 8601)', optional: true }, + startTime: { type: 'string', description: 'Trip start time (HH:mm)', optional: true }, + endTime: { type: 'string', description: 'Trip end time (HH:mm)', optional: true }, + approved: { + type: 'boolean', + description: 'Whether the request is approved', + optional: true, + }, + pendingApproval: { type: 'boolean', description: 'Pending approval flag', optional: true }, + closed: { type: 'boolean', description: 'Closed flag', optional: true }, + everSentBack: { type: 'boolean', description: 'Ever-sent-back flag', optional: true }, + canceledPostApproval: { + type: 'boolean', + description: 'Canceled after approval flag', + optional: true, + }, + approvalStatus: { + type: 'json', + description: 'Approval status', + optional: true, + properties: { + code: { + type: 'string', + description: 'Status code (NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK)', + optional: true, + }, + name: { type: 'string', description: 'Localized status name', optional: true }, + }, + }, + owner: { + type: 'json', + description: 'Travel request owner', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { type: 'string', description: 'Owner first name', optional: true }, + lastName: { type: 'string', description: 'Owner last name', optional: true }, + }, + }, + approver: { + type: 'json', + description: 'Approver assigned to the request', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { type: 'string', description: 'Approver first name', optional: true }, + lastName: { type: 'string', description: 'Approver last name', optional: true }, + }, + }, + policy: { + type: 'json', + description: 'Resource link to the applicable policy', + optional: true, + properties: { + id: { type: 'string', description: 'Policy ID', optional: true }, + href: { type: 'string', description: 'Policy hyperlink', optional: true }, + }, + }, + type: { + type: 'json', + description: 'Request type', + optional: true, + properties: { + code: { type: 'string', description: 'Request type code', optional: true }, + label: { type: 'string', description: 'Request type label', optional: true }, + }, + }, + mainDestination: { + type: 'json', + description: 'Main destination of the trip', + optional: true, + properties: { + city: { type: 'string', description: 'City', optional: true }, + countryCode: { type: 'string', description: 'ISO country code', optional: true }, + countrySubDivisionCode: { + type: 'string', + description: 'ISO country sub-division code', + optional: true, + }, + name: { type: 'string', description: 'Destination name', optional: true }, + }, + }, + totalApprovedAmount: { + type: 'json', + description: 'Total approved amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + totalPostedAmount: { + type: 'json', + description: 'Total posted amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + totalRemainingAmount: { + type: 'json', + description: 'Total remaining amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + operations: { + type: 'array', + description: 'Available workflow actions', + optional: true, + items: { type: 'json' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/update_user.ts b/apps/sim/tools/sap_concur/update_user.ts new file mode 100644 index 00000000000..45a2920095f --- /dev/null +++ b/apps/sim/tools/sap_concur/update_user.ts @@ -0,0 +1,95 @@ +import type { SapConcurProxyResponse, UpdateUserParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + scimUserOutputProperties, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateUserTool: ToolConfig = { + id: 'sap_concur_update_user', + name: 'SAP Concur Update User', + description: 'Patch a user identity (PATCH /profile/identity/v4.1/Users/{id}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User UUID to update', + }, + body: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'SCIM PATCH operations payload ({ schemas, Operations: [...] })', + }, + }, + request: { + url: SAP_CONCUR_PROXY_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userUuid = trimRequired(params.userUuid, 'userUuid') + return { + ...baseProxyBody(params), + path: `/profile/identity/v4.1/Users/${encodeURIComponent(userUuid)}`, + method: 'PATCH', + body: params.body, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Updated SCIM User payload', + properties: scimUserOutputProperties, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/upload_receipt_image.ts b/apps/sim/tools/sap_concur/upload_receipt_image.ts new file mode 100644 index 00000000000..66f19173035 --- /dev/null +++ b/apps/sim/tools/sap_concur/upload_receipt_image.ts @@ -0,0 +1,119 @@ +import type { SapConcurProxyResponse, UploadReceiptImageParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const SAP_CONCUR_UPLOAD_URL = '/api/tools/sap_concur/upload' + +export const uploadReceiptImageTool: ToolConfig = + { + id: 'sap_concur_upload_receipt_image', + name: 'SAP Concur Upload Receipt Image', + description: + 'Upload an image-only receipt (POST /receipts/v4/users/{userId}/image-only-receipts).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default), password, refresh_token', + }, + clientId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client ID', + }, + clientSecret: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Concur OAuth client secret', + }, + username: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Username (only for password grant)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password (only for password grant)', + }, + companyUuid: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Company UUID for multi-company access tokens', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Concur user UUID who owns the receipt', + }, + receipt: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Receipt image file (UserFile reference). Supported formats: PDF, PNG, JPEG, GIF, TIFF', + }, + forwardId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Optional client-supplied dedup id (max 40 chars). Sent as the concur-forwardid header.', + }, + }, + request: { + url: SAP_CONCUR_UPLOAD_URL, + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const userId = trimRequired(params.userId, 'userId') + return { + ...baseProxyBody(params), + operation: 'upload_receipt_image', + userId, + receipt: params.receipt, + forwardId: params.forwardId, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: + 'Image-only receipt upload response (HTTP 202 Accepted; Location and Link response headers exposed in body)', + properties: { + location: { + type: 'string', + description: + 'Location header URL for the new receipt image (e.g. /receipts/v4/images/{receiptId})', + optional: true, + }, + link: { + type: 'string', + description: 'Link header URL pointing to /receipts/v4/status/{receiptId}', + optional: true, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/utils.ts b/apps/sim/tools/sap_concur/utils.ts new file mode 100644 index 00000000000..c3261a6edda --- /dev/null +++ b/apps/sim/tools/sap_concur/utils.ts @@ -0,0 +1,297 @@ +import type { SapConcurBaseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import type { OutputProperty } from '@/tools/types' + +export const scimUserOutputProperties: Record = { + id: { type: 'string', description: 'User UUID' }, + externalId: { + type: 'string', + description: 'External identifier set by the provisioning client', + optional: true, + }, + userName: { type: 'string', description: 'Unique username (often email)' }, + displayName: { type: 'string', description: 'Display name', optional: true }, + nickName: { type: 'string', description: 'Casual or alternate name', optional: true }, + title: { type: 'string', description: 'Job title', optional: true }, + userType: { type: 'string', description: 'User type (e.g., Employee)', optional: true }, + preferredLanguage: { type: 'string', description: 'Preferred language tag', optional: true }, + locale: { type: 'string', description: 'Locale (e.g., en-US)', optional: true }, + timezone: { type: 'string', description: 'Timezone (e.g., America/Los_Angeles)', optional: true }, + active: { type: 'boolean', description: 'Whether the user is active', optional: true }, + dateOfBirth: { type: 'string', description: 'Date of birth (YYYY-MM-DD)', optional: true }, + name: { + type: 'json', + description: 'Structured name', + optional: true, + properties: { + formatted: { type: 'string', description: 'Formatted full name', optional: true }, + familyName: { type: 'string', description: 'Family (last) name', optional: true }, + familyNamePrefix: { type: 'string', description: 'Family name prefix', optional: true }, + givenName: { type: 'string', description: 'Given (first) name', optional: true }, + middleName: { type: 'string', description: 'Middle name', optional: true }, + honorificPrefix: { type: 'string', description: 'Honorific prefix', optional: true }, + honorificSuffix: { type: 'string', description: 'Honorific suffix', optional: true }, + }, + }, + emails: { + type: 'array', + description: 'Email addresses', + optional: true, + items: { + type: 'json', + properties: { + value: { type: 'string', description: 'Email address' }, + type: { type: 'string', description: 'Type (e.g., work, home)', optional: true }, + primary: { type: 'boolean', description: 'Primary email flag', optional: true }, + display: { type: 'string', description: 'Display label', optional: true }, + notifications: { + type: 'boolean', + description: 'Whether email notifications are enabled', + optional: true, + }, + verified: { type: 'boolean', description: 'Whether the email is verified', optional: true }, + }, + }, + }, + phoneNumbers: { + type: 'array', + description: 'Phone numbers', + optional: true, + items: { + type: 'json', + properties: { + value: { type: 'string', description: 'Phone number' }, + type: { type: 'string', description: 'Type (work, mobile, fax, etc.)', optional: true }, + primary: { type: 'boolean', description: 'Primary phone flag', optional: true }, + display: { type: 'string', description: 'Display label', optional: true }, + notifications: { + type: 'boolean', + description: 'Whether SMS notifications are enabled', + optional: true, + }, + }, + }, + }, + addresses: { + type: 'array', + description: 'Addresses', + optional: true, + items: { + type: 'json', + properties: { + type: { type: 'string', description: 'Address type (work, home, etc.)', optional: true }, + formatted: { type: 'string', description: 'Formatted address', optional: true }, + streetAddress: { type: 'string', description: 'Street address', optional: true }, + locality: { type: 'string', description: 'City / locality', optional: true }, + region: { type: 'string', description: 'State / region', optional: true }, + postalCode: { type: 'string', description: 'Postal code', optional: true }, + country: { type: 'string', description: 'ISO 3166-1 country code', optional: true }, + primary: { type: 'boolean', description: 'Primary address flag', optional: true }, + }, + }, + }, + entitlements: { + type: 'array', + description: 'Entitlements granted to the user', + optional: true, + items: { type: 'json' }, + }, + roles: { + type: 'array', + description: 'Roles assigned to the user', + optional: true, + items: { type: 'json' }, + }, + schemas: { + type: 'array', + description: 'SCIM schemas the resource conforms to', + optional: true, + items: { type: 'string' }, + }, + meta: { + type: 'json', + description: 'Resource metadata', + optional: true, + properties: { + created: { type: 'string', description: 'Creation timestamp', optional: true }, + lastModified: { type: 'string', description: 'Last modified timestamp', optional: true }, + resourceType: { type: 'string', description: 'Resource type (User)', optional: true }, + location: { type: 'string', description: 'Resource URL', optional: true }, + version: { type: 'string', description: 'ETag version', optional: true }, + }, + }, + emergencyContacts: { + type: 'array', + description: 'Emergency contacts', + optional: true, + items: { + type: 'json', + properties: { + name: { type: 'string', description: 'Contact full name', optional: true }, + relationship: { type: 'string', description: 'Relationship to user', optional: true }, + emails: { type: 'array', description: 'Emails', optional: true, items: { type: 'json' } }, + phones: { type: 'array', description: 'Phones', optional: true, items: { type: 'json' } }, + streetAddress: { type: 'string', description: 'Street address', optional: true }, + locality: { type: 'string', description: 'City / locality', optional: true }, + region: { type: 'string', description: 'State / region', optional: true }, + postalCode: { type: 'string', description: 'Postal code', optional: true }, + country: { type: 'string', description: 'ISO 3166-1 country code', optional: true }, + }, + }, + }, + localeOverrides: { + type: 'json', + description: 'Read-only locale and date/time/number preference overrides', + optional: true, + }, + 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User': { + type: 'json', + description: 'SCIM Enterprise User extension', + optional: true, + properties: { + employeeNumber: { type: 'string', description: 'Employee number', optional: true }, + companyId: { type: 'string', description: 'Concur company identifier', optional: true }, + startDate: { type: 'string', description: 'Employment start date', optional: true }, + terminationDate: { + type: 'string', + description: 'Employment termination date', + optional: true, + }, + leavesOfAbsence: { + type: 'array', + description: 'Leaves of absence', + optional: true, + items: { + type: 'json', + properties: { + startDate: { type: 'string', description: 'Leave start date', optional: true }, + endDate: { type: 'string', description: 'Leave end date', optional: true }, + type: { type: 'string', description: 'Leave type', optional: true }, + }, + }, + }, + costCenter: { type: 'string', description: 'Cost center', optional: true }, + organization: { type: 'string', description: 'Organization', optional: true }, + division: { type: 'string', description: 'Division', optional: true }, + department: { type: 'string', description: 'Department', optional: true }, + manager: { + type: 'json', + description: 'Manager reference', + optional: true, + properties: { + value: { type: 'string', description: 'Manager UUID', optional: true }, + $ref: { type: 'string', description: 'Manager resource URL', optional: true }, + displayName: { type: 'string', description: 'Manager display name', optional: true }, + employeeNumber: { + type: 'string', + description: 'Manager employee number', + optional: true, + }, + }, + }, + }, + }, + 'urn:ietf:params:scim:schemas:extension:sap:2.0:User': { + type: 'json', + description: 'SAP SCIM extension', + optional: true, + properties: { + userUuid: { type: 'string', description: 'SAP global user UUID', optional: true }, + }, + }, + 'urn:ietf:params:scim:schemas:extension:sap:concur:2.0:User': { + type: 'json', + description: 'SAP Concur SCIM extension (Concur-specific attributes)', + optional: true, + }, +} + +export const scimListResponseOutputProperties: Record = { + schemas: { + type: 'array', + description: 'SCIM schemas the response conforms to', + optional: true, + items: { type: 'string' }, + }, + totalResults: { + type: 'number', + description: 'Total number of results matching the query', + optional: true, + }, + itemsPerPage: { + type: 'number', + description: 'Number of results returned in this page', + optional: true, + }, + startIndex: { + type: 'number', + description: '1-based index of the first result', + optional: true, + }, + cursor: { + type: 'string', + description: 'SCIM v4.1 cursor for the next page of results', + optional: true, + }, + Resources: { + type: 'array', + description: 'SCIM User resources', + optional: true, + items: { + type: 'json', + properties: scimUserOutputProperties, + }, + }, +} + +export const SAP_CONCUR_PROXY_URL = '/api/tools/sap_concur/proxy' + +export function baseProxyBody(params: SapConcurBaseParams): Record { + const body: Record = { + datacenter: params.datacenter ?? 'us.api.concursolutions.com', + grantType: params.grantType ?? 'client_credentials', + clientId: params.clientId, + clientSecret: params.clientSecret, + } + if (params.username) body.username = params.username + if (params.password) body.password = params.password + if (params.companyUuid) body.companyUuid = params.companyUuid + return body +} + +export function buildListQuery( + params: Record +): Record { + const query: Record = {} + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null) continue + if (typeof value === 'string' && value.trim() === '') continue + query[key] = value + } + return query +} + +export async function transformSapConcurProxyResponse( + response: Response +): Promise { + const data = (await response.json()) as + | { success: true; output: { status: number; data: unknown } } + | { success: false; error?: string; status?: number } + if (!('success' in data) || data.success === false) { + const errMessage = 'error' in data && data.error ? data.error : 'Concur request failed' + throw new Error(errMessage) + } + return { + success: true, + output: { + status: data.output.status, + data: data.output.data, + }, + } +} + +export function trimRequired(value: string | undefined, name: string): string { + if (!value || !value.trim()) { + throw new Error(`${name} is required`) + } + return value.trim() +} diff --git a/apps/sim/tools/sap_s4hana/create_business_partner.ts b/apps/sim/tools/sap_s4hana/create_business_partner.ts index c908a2e118b..09f92ce8d0f 100644 --- a/apps/sim/tools/sap_s4hana/create_business_partner.ts +++ b/apps/sim/tools/sap_s4hana/create_business_partner.ts @@ -17,26 +17,26 @@ export const createBusinessPartnerTool: ToolConfig = params: { subdomain: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)', }, region: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'BTP region (e.g. eu10, us10)', }, clientId: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client ID from the S/4HANA Communication Arrangement', }, clientSecret: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client secret from the S/4HANA Communication Arrangement', }, @@ -111,6 +111,43 @@ export const getCustomerTool: ToolConfig = transformResponse: transformSapProxyResponse, outputs: { status: { type: 'number', description: 'HTTP status code returned by SAP' }, - data: { type: 'json', description: 'A_Customer entity' }, + data: { + type: 'object', + description: 'A_Customer entity', + properties: { + Customer: { type: 'string', description: 'Customer key (up to 10 characters)' }, + CustomerName: { type: 'string', description: 'Name of customer' }, + CustomerFullName: { type: 'string', description: 'Full name of the customer' }, + CustomerAccountGroup: { type: 'string', description: 'Customer account group' }, + CustomerClassification: { type: 'string', description: 'Customer classification code' }, + CustomerCorporateGroup: { type: 'string', description: 'Corporate group code' }, + AuthorizationGroup: { type: 'string', description: 'Authorization group' }, + Supplier: { type: 'string', description: 'Linked supplier account number' }, + FiscalAddress: { type: 'string', description: 'Fiscal address ID' }, + Industry: { type: 'string', description: 'Industry key' }, + NielsenRegion: { type: 'string', description: 'Nielsen ID' }, + ResponsibleType: { type: 'string', description: 'Responsible type' }, + NFPartnerIsNaturalPerson: { type: 'string', description: 'Natural person indicator' }, + InternationalLocationNumber1: { + type: 'string', + description: 'International location number 1', + }, + TaxNumberType: { type: 'string', description: 'Tax number type' }, + VATRegistration: { type: 'string', description: 'VAT registration number' }, + DeletionIndicator: { type: 'boolean', description: 'Central deletion flag' }, + OrderIsBlockedForCustomer: { + type: 'string', + description: 'Central order block reason code', + }, + PostingIsBlocked: { type: 'boolean', description: 'Central posting block flag' }, + DeliveryIsBlocked: { type: 'string', description: 'Central delivery block reason code' }, + BillingIsBlockedForCustomer: { + type: 'string', + description: 'Central billing block reason code', + }, + CreationDate: { type: 'string', description: 'Creation date (OData v2 epoch)' }, + CreatedByUser: { type: 'string', description: 'User who created the customer' }, + }, + }, }, } diff --git a/apps/sim/tools/sap_s4hana/get_inbound_delivery.ts b/apps/sim/tools/sap_s4hana/get_inbound_delivery.ts index 78d78a24596..7fe3ee75f16 100644 --- a/apps/sim/tools/sap_s4hana/get_inbound_delivery.ts +++ b/apps/sim/tools/sap_s4hana/get_inbound_delivery.ts @@ -17,26 +17,26 @@ export const getInboundDeliveryTool: ToolConfig = { params: { subdomain: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)', }, region: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'BTP region (e.g. eu10, us10)', }, clientId: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client ID from the S/4HANA Communication Arrangement', }, clientSecret: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client secret from the S/4HANA Communication Arrangement', }, @@ -110,6 +110,103 @@ export const getProductTool: ToolConfig = { transformResponse: transformSapProxyResponse, outputs: { status: { type: 'number', description: 'HTTP status code returned by SAP' }, - data: { type: 'json', description: 'A_Product entity' }, + data: { + type: 'json', + description: 'OData v2 response envelope; entity at output.data.d', + properties: { + d: { + type: 'json', + description: 'A_Product entity', + properties: { + Product: { + type: 'string', + description: 'Product (material) number', + optional: true, + }, + ProductType: { + type: 'string', + description: 'Product type (e.g., FERT, HAWA)', + optional: true, + }, + ProductGroup: { type: 'string', description: 'Material group', optional: true }, + BaseUnit: { type: 'string', description: 'Base unit of measure', optional: true }, + Brand: { type: 'string', description: 'Brand', optional: true }, + Division: { type: 'string', description: 'Division', optional: true }, + GrossWeight: { type: 'string', description: 'Gross weight', optional: true }, + NetWeight: { type: 'string', description: 'Net weight', optional: true }, + WeightUnit: { + type: 'string', + description: 'Weight unit of measure', + optional: true, + }, + CrossPlantStatus: { + type: 'string', + description: 'Cross-plant material status', + optional: true, + }, + IsMarkedForDeletion: { + type: 'boolean', + description: 'Deletion flag', + optional: true, + }, + ProductStandardID: { + type: 'string', + description: 'Standard product ID (e.g., GTIN)', + optional: true, + }, + ItemCategoryGroup: { + type: 'string', + description: 'Item category group', + optional: true, + }, + ProductOldID: { + type: 'string', + description: 'Legacy/old product ID', + optional: true, + }, + CreatedByUser: { + type: 'string', + description: 'User who created the product', + optional: true, + }, + CreationDate: { + type: 'string', + description: 'Creation date (OData /Date(ms)/)', + optional: true, + }, + LastChangedByUser: { + type: 'string', + description: 'User who last changed the product', + optional: true, + }, + LastChangeDate: { + type: 'string', + description: 'Last change date', + optional: true, + }, + LastChangeDateTime: { + type: 'string', + description: 'Last change timestamp (Edm.DateTimeOffset)', + optional: true, + }, + to_Description: { + type: 'json', + description: 'Product descriptions (when $expand=to_Description)', + optional: true, + }, + to_Plant: { + type: 'json', + description: 'Plant-level data (when $expand=to_Plant)', + optional: true, + }, + to_ProductSales: { + type: 'json', + description: 'Sales data (when $expand=to_ProductSales)', + optional: true, + }, + }, + }, + }, + }, }, } diff --git a/apps/sim/tools/sap_s4hana/get_purchase_order.ts b/apps/sim/tools/sap_s4hana/get_purchase_order.ts index 3a97e272113..e69f45ebeac 100644 --- a/apps/sim/tools/sap_s4hana/get_purchase_order.ts +++ b/apps/sim/tools/sap_s4hana/get_purchase_order.ts @@ -17,26 +17,26 @@ export const getPurchaseOrderTool: ToolConfig ({ ...baseProxyBody(params), service: 'API_PURCHASEORDER_PROCESS_SRV', - path: `/A_PurchaseOrder(${quoteOdataKey(params.purchaseOrder)})`, + path: `/A_PurchaseOrder(${quoteOdataKey(params.purchaseOrder.trim())})`, method: 'GET', query: buildEntityQuery(params), }), @@ -110,6 +110,78 @@ export const getPurchaseOrderTool: ToolConfig = params: { subdomain: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)', }, region: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'BTP region (e.g. eu10, us10)', }, clientId: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client ID from the S/4HANA Communication Arrangement', }, clientSecret: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client secret from the S/4HANA Communication Arrangement', }, @@ -111,6 +111,166 @@ export const getSupplierTool: ToolConfig = transformResponse: transformSapProxyResponse, outputs: { status: { type: 'number', description: 'HTTP status code returned by SAP' }, - data: { type: 'json', description: 'A_Supplier entity' }, + data: { + type: 'json', + description: 'OData v2 response envelope; entity at output.data.d', + properties: { + d: { + type: 'json', + description: 'A_Supplier entity', + properties: { + Supplier: { type: 'string', description: 'Supplier key (up to 10 characters)' }, + AlternativePayeeAccountNumber: { + type: 'string', + description: 'Account number of the alternative payee', + optional: true, + }, + AuthorizationGroup: { + type: 'string', + description: 'Authorization group', + optional: true, + }, + BusinessPartner: { + type: 'string', + description: 'Linked BusinessPartner key', + optional: true, + }, + BR_TaxIsSplit: { + type: 'boolean', + description: 'Brazil-specific tax split flag', + optional: true, + }, + CreatedByUser: { + type: 'string', + description: 'User who created the supplier', + optional: true, + }, + CreationDate: { + type: 'string', + description: 'Creation date (OData v2 epoch)', + optional: true, + }, + Customer: { + type: 'string', + description: 'Linked customer key (if any)', + optional: true, + }, + DeletionIndicator: { + type: 'boolean', + description: 'Central deletion flag', + optional: true, + }, + BirthDate: { + type: 'string', + description: 'Date of birth (OData v2 epoch)', + optional: true, + }, + ConcatenatedInternationalLocNo: { + type: 'string', + description: 'Concatenated international location number', + optional: true, + }, + FiscalAddress: { + type: 'string', + description: 'Fiscal address number', + optional: true, + }, + Industry: { type: 'string', description: 'Industry key', optional: true }, + InternationalLocationNumber1: { + type: 'string', + description: 'International location number, part 1', + optional: true, + }, + InternationalLocationNumber2: { + type: 'string', + description: 'International location number, part 2', + optional: true, + }, + InternationalLocationNumber3: { + type: 'string', + description: 'International location number, part 3', + optional: true, + }, + IsNaturalPerson: { + type: 'boolean', + description: 'Indicates whether the supplier is a natural person', + optional: true, + }, + PaymentIsBlockedForSupplier: { + type: 'boolean', + description: 'Payment block flag', + optional: true, + }, + PostingIsBlocked: { + type: 'boolean', + description: 'Posting block flag', + optional: true, + }, + PurchasingIsBlocked: { + type: 'boolean', + description: 'Purchasing block flag', + optional: true, + }, + ResponsibleType: { + type: 'string', + description: 'Type of business (Brazil)', + optional: true, + }, + SupplierAccountGroup: { + type: 'string', + description: 'Supplier account group', + optional: true, + }, + SupplierCorporateGroup: { + type: 'string', + description: 'Corporate group identifier', + optional: true, + }, + SupplierFullName: { + type: 'string', + description: 'Full name of the supplier', + optional: true, + }, + SupplierName: { type: 'string', description: 'Supplier name', optional: true }, + SupplierProcurementBlock: { + type: 'string', + description: 'Procurement block at supplier level', + optional: true, + }, + SuplrProofOfDelivRlvtCode: { + type: 'string', + description: 'Proof of delivery relevance code', + optional: true, + }, + SuplrQltyInProcmtCertfnValidTo: { + type: 'string', + description: 'Quality certification validity end date (OData v2 epoch)', + optional: true, + }, + SuplrQualityManagementSystem: { + type: 'string', + description: 'Quality management system of the supplier', + optional: true, + }, + TaxNumber1: { type: 'string', description: 'Tax number 1', optional: true }, + TaxNumber2: { type: 'string', description: 'Tax number 2', optional: true }, + TaxNumber3: { type: 'string', description: 'Tax number 3', optional: true }, + TaxNumber4: { type: 'string', description: 'Tax number 4', optional: true }, + TaxNumber5: { type: 'string', description: 'Tax number 5', optional: true }, + TaxNumberResponsible: { + type: 'string', + description: 'Tax number of responsible party', + optional: true, + }, + TaxNumberType: { type: 'string', description: 'Tax number type', optional: true }, + VATRegistration: { + type: 'string', + description: 'VAT registration number', + optional: true, + }, + }, + }, + }, + }, }, } diff --git a/apps/sim/tools/sap_s4hana/get_supplier_invoice.ts b/apps/sim/tools/sap_s4hana/get_supplier_invoice.ts index b27bb533e5b..2c66d1adeda 100644 --- a/apps/sim/tools/sap_s4hana/get_supplier_invoice.ts +++ b/apps/sim/tools/sap_s4hana/get_supplier_invoice.ts @@ -17,26 +17,26 @@ export const getSupplierInvoiceTool: ToolConfig ({ ...baseProxyBody(params), service: 'API_SUPPLIERINVOICE_PROCESS_SRV', - path: `/A_SupplierInvoice(FiscalYear=${quoteOdataKey(params.fiscalYear)},SupplierInvoice=${quoteOdataKey(params.supplierInvoice)})`, + path: `/A_SupplierInvoice(SupplierInvoice=${quoteOdataKey(params.supplierInvoice)},FiscalYear=${quoteOdataKey(params.fiscalYear)})`, method: 'GET', query: buildEntityQuery(params), }), @@ -116,6 +116,81 @@ export const getSupplierInvoiceTool: ToolConfig params: { subdomain: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)', }, region: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'BTP region (e.g. eu10, us10)', }, clientId: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client ID from the S/4HANA Communication Arrangement', }, clientSecret: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client secret from the S/4HANA Communication Arrangement', }, @@ -127,6 +127,123 @@ export const listProductsTool: ToolConfig transformResponse: transformSapProxyResponse, outputs: { status: { type: 'number', description: 'HTTP status code returned by SAP' }, - data: { type: 'json', description: 'Array of A_Product entities' }, + data: { + type: 'json', + description: 'OData v2 response envelope; collection at output.data.d.results', + properties: { + d: { + type: 'json', + description: 'OData v2 envelope', + properties: { + results: { + type: 'array', + description: 'A_Product entities', + items: { + type: 'object', + properties: { + Product: { + type: 'string', + description: 'Product (material) number', + optional: true, + }, + ProductType: { + type: 'string', + description: 'Product type (e.g., FERT, HAWA)', + optional: true, + }, + ProductGroup: { + type: 'string', + description: 'Material group', + optional: true, + }, + BaseUnit: { + type: 'string', + description: 'Base unit of measure', + optional: true, + }, + Brand: { type: 'string', description: 'Brand', optional: true }, + Division: { type: 'string', description: 'Division', optional: true }, + GrossWeight: { + type: 'string', + description: 'Gross weight', + optional: true, + }, + NetWeight: { + type: 'string', + description: 'Net weight', + optional: true, + }, + WeightUnit: { + type: 'string', + description: 'Weight unit of measure', + optional: true, + }, + CrossPlantStatus: { + type: 'string', + description: 'Cross-plant material status', + optional: true, + }, + IsMarkedForDeletion: { + type: 'boolean', + description: 'Deletion flag', + optional: true, + }, + ProductStandardID: { + type: 'string', + description: 'Standard product ID (e.g., GTIN)', + optional: true, + }, + ItemCategoryGroup: { + type: 'string', + description: 'Item category group', + optional: true, + }, + ProductOldID: { + type: 'string', + description: 'Legacy/old product ID', + optional: true, + }, + CreatedByUser: { + type: 'string', + description: 'User who created the product', + optional: true, + }, + CreationDate: { + type: 'string', + description: 'Creation date (OData /Date(ms)/)', + optional: true, + }, + LastChangedByUser: { + type: 'string', + description: 'User who last changed the product', + optional: true, + }, + LastChangeDate: { + type: 'string', + description: 'Last change date', + optional: true, + }, + LastChangeDateTime: { + type: 'string', + description: 'Last change timestamp (Edm.DateTimeOffset)', + optional: true, + }, + }, + }, + }, + __next: { + type: 'string', + description: 'OData skiptoken URL for next page', + optional: true, + }, + __count: { + type: 'string', + description: 'Total count when $inlinecount=allpages is used', + optional: true, + }, + }, + }, + }, + }, }, } diff --git a/apps/sim/tools/sap_s4hana/list_purchase_orders.ts b/apps/sim/tools/sap_s4hana/list_purchase_orders.ts index f3e2c7778d9..fc2e829ab43 100644 --- a/apps/sim/tools/sap_s4hana/list_purchase_orders.ts +++ b/apps/sim/tools/sap_s4hana/list_purchase_orders.ts @@ -16,26 +16,26 @@ export const listPurchaseOrdersTool: ToolConfig = { id: 'sap_s4hana_odata_query', name: 'SAP S/4HANA OData Query', description: - 'Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping.', + 'Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping. For write operations (POST/PUT/PATCH/MERGE/DELETE), pass an If-Match ETag obtained from a prior GET to avoid lost updates; misuse will mutate production data.', version: '1.0.0', params: { subdomain: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)', }, region: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'BTP region (e.g. eu10, us10)', }, clientId: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client ID from the S/4HANA Communication Arrangement', }, clientSecret: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client secret from the S/4HANA Communication Arrangement', }, diff --git a/apps/sim/tools/sap_s4hana/update_business_partner.ts b/apps/sim/tools/sap_s4hana/update_business_partner.ts index 41dd9b5165d..cb37c577f0d 100644 --- a/apps/sim/tools/sap_s4hana/update_business_partner.ts +++ b/apps/sim/tools/sap_s4hana/update_business_partner.ts @@ -13,31 +13,31 @@ export const updateBusinessPartnerTool: ToolConfig Date: Wed, 6 May 2026 17:54:57 -0700 Subject: [PATCH 02/13] added --- apps/docs/content/docs/en/tools/sap_concur.mdx | 18 +++++++----------- apps/docs/content/docs/en/tools/sharepoint.mdx | 10 ++++++++++ .../integrations/data/integrations.json | 4 ++-- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/apps/docs/content/docs/en/tools/sap_concur.mdx b/apps/docs/content/docs/en/tools/sap_concur.mdx index 9547354bc17..eb021460ef3 100644 --- a/apps/docs/content/docs/en/tools/sap_concur.mdx +++ b/apps/docs/content/docs/en/tools/sap_concur.mdx @@ -42,7 +42,7 @@ Connect SAP Concur via OAuth 2.0. Manage expense reports and line items, allocat ### `sap_concur_approve_expense_report` -Approve an expense report as a manager (PATCH /expensereports/v4/users/{userId}/context/MANAGER/reports/{reportId}/approve). +Approve an expense report as a manager (PATCH /expensereports/v4/reports/{reportId}/approve). Required body field: comment. #### Input @@ -55,8 +55,6 @@ Approve an expense report as a manager (PATCH /expensereports/v4/users/{userId}/ | `username` | string | No | Username \(only for password grant\) | | `password` | string | No | Password \(only for password grant\) | | `companyUuid` | string | No | Company UUID for multi-company access tokens | -| `userId` | string | Yes | Manager user UUID approving the report | -| `contextType` | string | Yes | Access context: must be MANAGER | | `reportId` | string | Yes | Expense report ID to approve | | `body` | json | Yes | Request body — `comment` is required by Concur \(e.g., \{ "comment": "Approved" \}\). If the report contains rejected expenses, `expenseRejectedComment` is also required. Optional fields: `expectedStepCode`, `expectedStepSequence`, `statusId` \(defaults to "A_APPR"\). | @@ -98,7 +96,7 @@ Associate attendees with an expense (POST /expensereports/v4/users/{userId}/cont ### `sap_concur_create_cash_advance` -Create a cash advance (POST /cashadvance/v4.1/cashadvances). +Create a cash advance (POST /cashadvance/v4/cashadvances). #### Input @@ -470,7 +468,7 @@ Delete an expected expense (DELETE /travelrequest/v4/expenses/{expenseUuid}). | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by Concur | -| `data` | json | Empty body on 204 No Content when the expected expense is deleted. Error payload otherwise. | +| `data` | json | Returns boolean true on 200 OK when the expected expense is deleted. | ### `sap_concur_delete_expense` @@ -688,7 +686,7 @@ Get a budget item header by ID (GET /budget/v4/budgetItemHeader/{id}). ### `sap_concur_get_cash_advance` -Get a cash advance (GET /cashadvance/v4.1/cashadvances/{cashadvanceId}). +Get a cash advance (GET /cashadvance/v4/cashadvances/{cashadvanceId}). #### Input @@ -1428,7 +1426,7 @@ Get a single user by UUID (GET /profile/identity/v4.1/Users/{id}). ### `sap_concur_issue_cash_advance` -Issue a cash advance (POST /cashadvance/v4.1/cashadvances/{cashadvanceId}/issue). +Issue a cash advance (POST /cashadvance/v4/cashadvances/{cashadvanceId}/issue). #### Input @@ -1450,7 +1448,7 @@ Issue a cash advance (POST /cashadvance/v4.1/cashadvances/{cashadvanceId}/issue) | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by Concur | | `data` | json | Issue cash advance result payload | -| ↳ `issuedDate` | string | Datetime the cash advance was issued \(UTC, YYYY-MM-DD hh:mm:ss\) | +| ↳ `issuedDate` | string | Date the cash advance was issued \(YYYY-MM-DD\) | | ↳ `status` | json | Cash advance status after the issue action | | ↳ `code` | string | Status code | | ↳ `name` | string | Status display name | @@ -2427,7 +2425,7 @@ Search users via SCIM .search endpoint (POST /profile/identity/v4.1/Users/.searc ### `sap_concur_send_back_expense_report` -Send back an expense report to the employee (PATCH /expensereports/v4/users/{userId}/context/MANAGER/reports/{reportId}/sendBack). +Send back an expense report to the employee (PATCH /expensereports/v4/reports/{reportId}/sendBack). Required body field: comment. #### Input @@ -2440,8 +2438,6 @@ Send back an expense report to the employee (PATCH /expensereports/v4/users/{use | `username` | string | No | Username \(only for password grant\) | | `password` | string | No | Password \(only for password grant\) | | `companyUuid` | string | No | Company UUID for multi-company access tokens | -| `userId` | string | Yes | Manager user UUID sending back the report | -| `contextType` | string | Yes | Access context: must be MANAGER | | `reportId` | string | Yes | Expense report ID to send back | | `body` | json | Yes | Request body — `comment` is required by Concur \(e.g., \{ "comment": "Missing receipt" \}\). Optional fields: `expectedStepCode`, `expectedStepSequence`. | diff --git a/apps/docs/content/docs/en/tools/sharepoint.mdx b/apps/docs/content/docs/en/tools/sharepoint.mdx index cf83987bd40..05a425ba35e 100644 --- a/apps/docs/content/docs/en/tools/sharepoint.mdx +++ b/apps/docs/content/docs/en/tools/sharepoint.mdx @@ -278,5 +278,15 @@ Upload files to a SharePoint document library | ↳ `createdDateTime` | string | When the file was created | | ↳ `lastModifiedDateTime` | string | When the file was last modified | | `fileCount` | number | Number of files uploaded | +| `skippedFiles` | array | Files that were skipped before upload | +| ↳ `name` | string | File name | +| ↳ `size` | number | File size in bytes | +| ↳ `limit` | number | Upload size limit in bytes | +| ↳ `reason` | string | Reason the file was skipped | +| `skippedCount` | number | Number of files skipped | +| `errors` | array | Per-file upload errors | +| ↳ `name` | string | File name | +| ↳ `error` | string | Error message | +| ↳ `status` | number | HTTP status from Microsoft Graph | diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 69245b5cb8b..7a4810b2d60 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -11522,11 +11522,11 @@ }, { "name": "Approve Expense Report", - "description": "Approve an expense report as a manager (PATCH /expensereports/v4/users/{userId}/context/MANAGER/reports/{reportId}/approve)." + "description": "Approve an expense report as a manager (PATCH /expensereports/v4/reports/{reportId}/approve). Required body field: comment." }, { "name": "Send Back Expense Report", - "description": "Send back an expense report to the employee (PATCH /expensereports/v4/users/{userId}/context/MANAGER/reports/{reportId}/sendBack)." + "description": "Send back an expense report to the employee (PATCH /expensereports/v4/reports/{reportId}/sendBack). Required body field: comment." }, { "name": "List Reports To Approve", From d767d76dd26d66144fa0d578e5023789cfe7fffe Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 18:15:16 -0700 Subject: [PATCH 03/13] fix(sap_s4hana): preserve raw Set-Cookie array for CSRF cookie join SecureFetchHeaders previously collapsed multi-value Set-Cookie headers with ", ", forcing consumers to re-split via a fragile regex. Cookie values containing "=" or "," (e.g., Base64 session tokens) could be misparsed and produce malformed Cookie strings on CSRF-protected mutations. Add SecureFetchHeaders.getSetCookie() that returns the raw array, and update the S/4HANA OData proxy's joinSetCookies to consume it directly. Co-Authored-By: Claude Opus 4.7 --- .../app/api/tools/sap_s4hana/proxy/route.ts | 4 +-- .../core/security/input-validation.server.ts | 27 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/api/tools/sap_s4hana/proxy/route.ts b/apps/sim/app/api/tools/sap_s4hana/proxy/route.ts index ae1e9c06054..bee4a8b84aa 100644 --- a/apps/sim/app/api/tools/sap_s4hana/proxy/route.ts +++ b/apps/sim/app/api/tools/sap_s4hana/proxy/route.ts @@ -113,8 +113,8 @@ interface CsrfBundle { } function joinSetCookies(response: SecureFetchResponse): string { - const cookies = (response.headers.get('set-cookie') ?? '').split(/,\s*(?=[^=,;\s]+=)/) - return cookies + return response.headers + .getSetCookie() .map((c) => c.split(';')[0]?.trim()) .filter(Boolean) .join('; ') diff --git a/apps/sim/lib/core/security/input-validation.server.ts b/apps/sim/lib/core/security/input-validation.server.ts index ed23140ea46..90c65eca62e 100644 --- a/apps/sim/lib/core/security/input-validation.server.ts +++ b/apps/sim/lib/core/security/input-validation.server.ts @@ -217,15 +217,22 @@ export interface SecureFetchOptions { export class SecureFetchHeaders { private headers: Map + private setCookies: string[] - constructor(headers: Record) { + constructor(headers: Record, setCookies: string[] = []) { this.headers = new Map(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])) + this.setCookies = setCookies } get(name: string): string | null { return this.headers.get(name.toLowerCase()) ?? null } + /** Returns the raw `Set-Cookie` header values as an array. Each entry is one cookie. */ + getSetCookie(): string[] { + return [...this.setCookies] + } + toRecord(): Record { const record: Record = {} for (const [key, value] of this.headers) { @@ -384,11 +391,21 @@ export async function secureFetchWithPinnedIP( const bodyBuffer = Buffer.concat(chunks) const body = bodyBuffer.toString('utf-8') const headersRecord: Record = {} + let setCookieArray: string[] = [] for (const [key, value] of Object.entries(res.headers)) { - if (typeof value === 'string') { - headersRecord[key.toLowerCase()] = value + const lowerKey = key.toLowerCase() + if (lowerKey === 'set-cookie') { + if (Array.isArray(value)) { + setCookieArray = value + headersRecord[lowerKey] = value.join(', ') + } else if (typeof value === 'string') { + setCookieArray = [value] + headersRecord[lowerKey] = value + } + } else if (typeof value === 'string') { + headersRecord[lowerKey] = value } else if (Array.isArray(value)) { - headersRecord[key.toLowerCase()] = value.join(', ') + headersRecord[lowerKey] = value.join(', ') } } @@ -396,7 +413,7 @@ export async function secureFetchWithPinnedIP( ok: statusCode >= 200 && statusCode < 300, status: statusCode, statusText: res.statusMessage || '', - headers: new SecureFetchHeaders(headersRecord), + headers: new SecureFetchHeaders(headersRecord, setCookieArray), text: async () => body, json: async () => JSON.parse(body), arrayBuffer: async () => From e10ab76e8050de44b6ac6646a5884294b303e4cc Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 18:17:27 -0700 Subject: [PATCH 04/13] fix(sap-concur): rename misleading exchange-rate tool, drop unusable refresh_token grant, validate geolocation host - Rename sap_concur_get_exchange_rate to sap_concur_upload_exchange_rates (POST bulk upload, not GET) - Remove refresh_token from SapConcurGrantType / Zod enum / block dropdown / docs (no implementation) - Validate Concur geolocation hostname against SAP_CONCUR_ALLOWED_DATACENTERS Co-Authored-By: Claude Opus 4.7 --- .../docs/content/docs/en/tools/sap_concur.mdx | 142 +++++++++--------- apps/sim/app/api/tools/sap_concur/shared.ts | 9 +- apps/sim/blocks/blocks/sap_concur.ts | 11 +- apps/sim/tools/registry.ts | 4 +- .../sap_concur/approve_expense_report.ts | 2 +- .../tools/sap_concur/associate_attendees.ts | 2 +- .../tools/sap_concur/create_cash_advance.ts | 2 +- .../sap_concur/create_expected_expense.ts | 2 +- .../tools/sap_concur/create_expense_report.ts | 2 +- apps/sim/tools/sap_concur/create_list_item.ts | 2 +- .../sap_concur/create_purchase_request.ts | 2 +- .../tools/sap_concur/create_quick_expense.ts | 2 +- .../create_quick_expense_with_image.ts | 2 +- .../tools/sap_concur/create_report_comment.ts | 2 +- .../tools/sap_concur/create_travel_request.ts | 2 +- apps/sim/tools/sap_concur/create_user.ts | 2 +- .../sap_concur/delete_expected_expense.ts | 2 +- apps/sim/tools/sap_concur/delete_expense.ts | 2 +- .../tools/sap_concur/delete_expense_report.ts | 2 +- apps/sim/tools/sap_concur/delete_list_item.ts | 2 +- .../tools/sap_concur/delete_travel_request.ts | 2 +- apps/sim/tools/sap_concur/delete_user.ts | 2 +- apps/sim/tools/sap_concur/get_allocation.ts | 2 +- apps/sim/tools/sap_concur/get_budget.ts | 2 +- apps/sim/tools/sap_concur/get_cash_advance.ts | 2 +- .../tools/sap_concur/get_expected_expense.ts | 2 +- apps/sim/tools/sap_concur/get_expense.ts | 2 +- .../tools/sap_concur/get_expense_report.ts | 2 +- apps/sim/tools/sap_concur/get_itemizations.ts | 2 +- apps/sim/tools/sap_concur/get_itinerary.ts | 2 +- apps/sim/tools/sap_concur/get_list.ts | 2 +- apps/sim/tools/sap_concur/get_list_item.ts | 2 +- .../tools/sap_concur/get_purchase_request.ts | 2 +- apps/sim/tools/sap_concur/get_receipt.ts | 2 +- .../tools/sap_concur/get_receipt_status.ts | 2 +- .../sap_concur/get_request_cash_advance.ts | 2 +- .../tools/sap_concur/get_travel_profile.ts | 2 +- .../tools/sap_concur/get_travel_request.ts | 2 +- apps/sim/tools/sap_concur/get_user.ts | 2 +- apps/sim/tools/sap_concur/index.ts | 2 +- .../tools/sap_concur/issue_cash_advance.ts | 2 +- apps/sim/tools/sap_concur/list_allocations.ts | 2 +- .../sap_concur/list_attendee_associations.ts | 2 +- .../sap_concur/list_budget_categories.ts | 2 +- apps/sim/tools/sap_concur/list_budgets.ts | 2 +- apps/sim/tools/sap_concur/list_exceptions.ts | 2 +- .../sap_concur/list_expected_expenses.ts | 2 +- .../tools/sap_concur/list_expense_reports.ts | 2 +- apps/sim/tools/sap_concur/list_expenses.ts | 2 +- apps/sim/tools/sap_concur/list_itineraries.ts | 2 +- apps/sim/tools/sap_concur/list_list_items.ts | 2 +- apps/sim/tools/sap_concur/list_lists.ts | 2 +- apps/sim/tools/sap_concur/list_receipts.ts | 2 +- .../tools/sap_concur/list_report_comments.ts | 2 +- .../sap_concur/list_reports_to_approve.ts | 2 +- .../list_travel_profiles_summary.ts | 2 +- .../list_travel_request_comments.ts | 2 +- .../tools/sap_concur/list_travel_requests.ts | 2 +- apps/sim/tools/sap_concur/list_users.ts | 2 +- .../tools/sap_concur/move_travel_request.ts | 2 +- .../tools/sap_concur/recall_expense_report.ts | 2 +- .../tools/sap_concur/remove_all_attendees.ts | 2 +- apps/sim/tools/sap_concur/search_locations.ts | 2 +- apps/sim/tools/sap_concur/search_users.ts | 2 +- .../sap_concur/send_back_expense_report.ts | 2 +- .../tools/sap_concur/submit_expense_report.ts | 2 +- apps/sim/tools/sap_concur/types.ts | 4 +- .../sim/tools/sap_concur/update_allocation.ts | 2 +- .../sap_concur/update_expected_expense.ts | 2 +- apps/sim/tools/sap_concur/update_expense.ts | 2 +- .../tools/sap_concur/update_expense_report.ts | 2 +- apps/sim/tools/sap_concur/update_list_item.ts | 2 +- .../tools/sap_concur/update_travel_request.ts | 2 +- apps/sim/tools/sap_concur/update_user.ts | 2 +- ...hange_rate.ts => upload_exchange_rates.ts} | 13 +- .../tools/sap_concur/upload_receipt_image.ts | 2 +- 76 files changed, 165 insertions(+), 158 deletions(-) rename apps/sim/tools/sap_concur/{get_exchange_rate.ts => upload_exchange_rates.ts} (90%) diff --git a/apps/docs/content/docs/en/tools/sap_concur.mdx b/apps/docs/content/docs/en/tools/sap_concur.mdx index eb021460ef3..4da75a6d362 100644 --- a/apps/docs/content/docs/en/tools/sap_concur.mdx +++ b/apps/docs/content/docs/en/tools/sap_concur.mdx @@ -49,7 +49,7 @@ Approve an expense report as a manager (PATCH /expensereports/v4/reports/{report | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -74,7 +74,7 @@ Associate attendees with an expense (POST /expensereports/v4/users/{userId}/cont | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -103,7 +103,7 @@ Create a cash advance (POST /cashadvance/v4/cashadvances). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -128,7 +128,7 @@ Create an expected expense on a travel request (POST /travelrequest/v4/requests/ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -169,7 +169,7 @@ Create an expense report (POST /expensereports/v4/users/{userId}/context/{contex | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -196,7 +196,7 @@ Create a list item (POST /list/v4/items). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -230,7 +230,7 @@ Create a purchase request (POST /purchaserequest/v4/purchaserequests). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -260,7 +260,7 @@ Create a quick expense (POST /quickexpense/v4/users/{userId}/context/TRAVELER/qu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -287,7 +287,7 @@ Create a quick expense with an attached image (POST /quickexpense/v4/users/{user | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -315,7 +315,7 @@ Create a comment on a report (POST /expensereports/v4/users/{userId}/context/{co | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -343,7 +343,7 @@ Create a travel request (POST /travelrequest/v4/requests). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -430,7 +430,7 @@ Create a new user identity (POST /profile/identity/v4.1/Users). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -454,7 +454,7 @@ Delete an expected expense (DELETE /travelrequest/v4/expenses/{expenseUuid}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -479,7 +479,7 @@ Delete an expense (DELETE /expensereports/v4/reports/{reportId}/expenses/{expens | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -504,7 +504,7 @@ Delete an expense report (DELETE /expensereports/v4/reports/{reportId}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -528,7 +528,7 @@ Delete a list item (DELETE /list/v4/items/{itemId}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -552,7 +552,7 @@ Delete a travel request (DELETE /travelrequest/v4/requests/{requestUuid}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -577,7 +577,7 @@ Delete a user identity (DELETE /profile/identity/v4.1/Users/{id}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -601,7 +601,7 @@ Get a single allocation (GET /expensereports/v4/users/{userId}/context/{contextT | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -645,7 +645,7 @@ Get a budget item header by ID (GET /budget/v4/budgetItemHeader/{id}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -693,7 +693,7 @@ Get a cash advance (GET /cashadvance/v4/cashadvances/{cashadvanceId}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -733,7 +733,7 @@ Get a cash advance (GET /cashadvance/v4/cashadvances/{cashadvanceId}). | ↳ `paymentCode` | string | Payment type code | | ↳ `description` | string | Payment method description | -### `sap_concur_get_exchange_rate` +### `sap_concur_upload_exchange_rates` Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body: { currency_sets: [{ from_crn_code, to_crn_code, start_date: @@ -742,7 +742,7 @@ Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body: | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -769,7 +769,7 @@ Get an expected expense (GET /travelrequest/v4/expenses/{expenseUuid}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -809,7 +809,7 @@ Get a single expense (GET /expensereports/v4/users/{userId}/context/{contextType | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -887,7 +887,7 @@ Retrieve a single expense report header by id via Expense Report v4 (/expenserep | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -981,7 +981,7 @@ Get expense itemizations (GET /expensereports/v4/users/{userId}/context/{context | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1029,7 +1029,7 @@ Get a single trip/itinerary (GET /api/travel/trip/v1.1/{tripID}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1079,7 +1079,7 @@ Get a single custom list (GET /list/v4/lists/{listId}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1115,7 +1115,7 @@ Get a single list item (GET /list/v4/items/{itemId}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1149,7 +1149,7 @@ Get a purchase request by ID (GET /purchaserequest/v4/purchaserequests/{id}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1185,7 +1185,7 @@ Get a single receipt by ID (GET /receipts/v4/{receiptId}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1217,7 +1217,7 @@ Get receipt processing status (GET /receipts/v4/status/{receiptId}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1246,7 +1246,7 @@ Get a travel profile (GET /api/travelprofile/v2.0/profile). Returns the calling | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1297,7 +1297,7 @@ Get a single travel request (GET /travelrequest/v4/requests/{requestUuid}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1407,7 +1407,7 @@ Get a single user by UUID (GET /profile/identity/v4.1/Users/{id}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1433,7 +1433,7 @@ Issue a cash advance (POST /cashadvance/v4/cashadvances/{cashadvanceId}/issue). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1462,7 +1462,7 @@ List allocations on an expense (GET /expensereports/v4/users/{userId}/context/{c | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1489,7 +1489,7 @@ List attendees associated with an expense (GET /expensereports/v4/users/{userId} | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1534,7 +1534,7 @@ List budget categories (GET /budget/v4/budgetCategory). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1557,7 +1557,7 @@ List budget item headers (GET /budget/v4/budgetItemHeader). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1586,7 +1586,7 @@ List exceptions on a report (GET /expensereports/v4/users/{userId}/context/{cont | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1619,7 +1619,7 @@ List expected expenses on a travel request (GET /travelrequest/v4/requests/{requ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1644,7 +1644,7 @@ List expenses on a report (GET /expensereports/v4/users/{userId}/context/{contex | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1700,7 +1700,7 @@ List expense reports (GET /api/v3.0/expense/reports). Returns a v3 envelope { It | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(us, us2, eu, eu2, cn, emea — defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1762,7 +1762,7 @@ List travel trips/itineraries (GET /api/travel/trip/v1.1). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1824,7 +1824,7 @@ List custom lists (GET /list/v4/lists). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1875,7 +1875,7 @@ List the top-level items (children) for a custom list (GET /list/v4/lists/{listI | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1926,7 +1926,7 @@ List receipts for a user (GET /receipts/v4/users/{userId}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1958,7 +1958,7 @@ List comments on a report (GET /expensereports/v4/users/{userId}/context/{contex | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -1998,7 +1998,7 @@ List expense reports awaiting approval (GET /expensereports/v4/users/{userId}/co | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2039,7 +2039,7 @@ Get a single cash advance assigned to a travel request (GET /travelrequest/v4/ca | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2075,7 +2075,7 @@ List travel profile summaries (GET /api/travelprofile/v2.0/summary). LastModifie | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2121,7 +2121,7 @@ List comments on a travel request (GET /travelrequest/v4/requests/{requestUuid}/ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2151,7 +2151,7 @@ List travel requests (GET /travelrequest/v4/requests). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2230,7 +2230,7 @@ List Concur user identities (GET /profile/identity/v4.1/Users). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2257,7 +2257,7 @@ Move a travel request through workflow (POST /travelrequest/v4/requests/{request | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2298,7 +2298,7 @@ Recall a submitted expense report (PATCH /expensereports/v4/users/{userId}/conte | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2325,7 +2325,7 @@ Remove all attendees from an expense (DELETE /expensereports/v4/users/{userId}/c | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2352,7 +2352,7 @@ Search Concur location reference data (GET /localities/v5/locations). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2408,7 +2408,7 @@ Search users via SCIM .search endpoint (POST /profile/identity/v4.1/Users/.searc | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2432,7 +2432,7 @@ Send back an expense report to the employee (PATCH /expensereports/v4/reports/{r | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2457,7 +2457,7 @@ Submit an expense report into the workflow via Expense Report v4 (PATCH /expense | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2483,7 +2483,7 @@ Update an allocation (PATCH /expensereports/v4/users/{userId}/context/{contextTy | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2511,7 +2511,7 @@ Update an expected expense (PUT /travelrequest/v4/expenses/{expenseUuid}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2552,7 +2552,7 @@ Update an expense (PATCH /expensereports/v4/reports/{reportId}/expenses/{expense | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2578,7 +2578,7 @@ Update an unsubmitted expense report (PATCH /expensereports/v4/users/{userId}/co | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2605,7 +2605,7 @@ Update a list item (PUT /list/v4/items/{itemId}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2640,7 +2640,7 @@ Update a travel request (PUT /travelrequest/v4/requests/{requestUuid}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2715,7 +2715,7 @@ Patch a user identity (PATCH /profile/identity/v4.1/Users/{id}). | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | @@ -2740,7 +2740,7 @@ Upload an image-only receipt (POST /receipts/v4/users/{userId}/image-only-receip | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | -| `grantType` | string | No | OAuth grant type: client_credentials \(default\), password, refresh_token | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | | `clientId` | string | Yes | Concur OAuth client ID | | `clientSecret` | string | Yes | Concur OAuth client secret | | `username` | string | No | Username \(only for password grant\) | diff --git a/apps/sim/app/api/tools/sap_concur/shared.ts b/apps/sim/app/api/tools/sap_concur/shared.ts index dd5fd5aa1b7..e395b4123af 100644 --- a/apps/sim/app/api/tools/sap_concur/shared.ts +++ b/apps/sim/app/api/tools/sap_concur/shared.ts @@ -22,7 +22,7 @@ export const SapConcurDatacenterSchema = z message: `datacenter must be one of: ${Array.from(SAP_CONCUR_ALLOWED_DATACENTERS).join(', ')}`, }) -export const SapConcurGrantTypeSchema = z.enum(['client_credentials', 'password', 'refresh_token']) +export const SapConcurGrantTypeSchema = z.enum(['client_credentials', 'password']) export const SapConcurAuthSchema = z.object({ datacenter: SapConcurDatacenterSchema.default('us.api.concursolutions.com'), @@ -257,7 +257,12 @@ export async function fetchSapConcurAccessToken( } const geolocation = normalizeGeolocation(data.geolocation, auth.datacenter) - assertSafeExternalUrl(geolocation, 'geolocation') + const geolocationUrl = assertSafeExternalUrl(geolocation, 'geolocation') + if (!SAP_CONCUR_ALLOWED_DATACENTERS.has(geolocationUrl.hostname.toLowerCase())) { + throw new Error( + `Concur geolocation host is not in the allowed datacenter list: ${geolocationUrl.hostname}` + ) + } const expiresInMs = (data.expires_in ?? 3600) * 1000 rememberToken(cacheKey, { diff --git a/apps/sim/blocks/blocks/sap_concur.ts b/apps/sim/blocks/blocks/sap_concur.ts index 54da09b093b..b67e2178f43 100644 --- a/apps/sim/blocks/blocks/sap_concur.ts +++ b/apps/sim/blocks/blocks/sap_concur.ts @@ -152,7 +152,7 @@ const BODY_OPS = [ 'sap_concur_update_user', 'sap_concur_search_users', 'sap_concur_create_purchase_request', - 'sap_concur_get_exchange_rate', + 'sap_concur_upload_exchange_rates', ] export const SapConcurBlock: BlockConfig = { @@ -247,7 +247,7 @@ export const SapConcurBlock: BlockConfig = { { label: 'List Budgets', id: 'sap_concur_list_budgets' }, { label: 'Get Budget', id: 'sap_concur_get_budget' }, { label: 'List Budget Categories', id: 'sap_concur_list_budget_categories' }, - { label: 'Get Exchange Rate', id: 'sap_concur_get_exchange_rate' }, + { label: 'Upload Exchange Rates', id: 'sap_concur_upload_exchange_rates' }, { label: 'Create Purchase Request', id: 'sap_concur_create_purchase_request' }, { label: 'Get Purchase Request', id: 'sap_concur_get_purchase_request' }, { label: 'Get Travel Profile', id: 'sap_concur_get_travel_profile' }, @@ -284,7 +284,6 @@ export const SapConcurBlock: BlockConfig = { options: [ { label: 'Client Credentials', id: 'client_credentials' }, { label: 'Password', id: 'password' }, - { label: 'Refresh Token', id: 'refresh_token' }, ], value: () => 'client_credentials', }, @@ -1240,7 +1239,7 @@ export const SapConcurBlock: BlockConfig = { 'sap_concur_update_user', 'sap_concur_search_users', 'sap_concur_create_purchase_request', - 'sap_concur_get_exchange_rate', + 'sap_concur_upload_exchange_rates', 'sap_concur_create_list_item', 'sap_concur_update_list_item', ], @@ -1270,7 +1269,7 @@ export const SapConcurBlock: BlockConfig = { 'sap_concur_get_allocation', 'sap_concur_get_budget', 'sap_concur_get_cash_advance', - 'sap_concur_get_exchange_rate', + 'sap_concur_upload_exchange_rates', 'sap_concur_get_expected_expense', 'sap_concur_get_expense', 'sap_concur_get_expense_report', @@ -1699,7 +1698,7 @@ export const SapConcurBlock: BlockConfig = { return { ...auth, budgetId: params.budgetId } case 'sap_concur_list_budget_categories': return { ...auth } - case 'sap_concur_get_exchange_rate': + case 'sap_concur_upload_exchange_rates': return { ...auth, body: params.body } case 'sap_concur_create_purchase_request': return { ...auth, body: params.body } diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 71601e636e3..8da147da4e7 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2292,7 +2292,6 @@ import { getAllocationTool as sapConcurGetAllocationTool, getBudgetTool as sapConcurGetBudgetTool, getCashAdvanceTool as sapConcurGetCashAdvanceTool, - getExchangeRateTool as sapConcurGetExchangeRateTool, getExpectedExpenseTool as sapConcurGetExpectedExpenseTool, getExpenseReportTool as sapConcurGetExpenseReportTool, getExpenseTool as sapConcurGetExpenseTool, @@ -2340,6 +2339,7 @@ import { updateListItemTool as sapConcurUpdateListItemTool, updateTravelRequestTool as sapConcurUpdateTravelRequestTool, updateUserTool as sapConcurUpdateUserTool, + uploadExchangeRatesTool as sapConcurUploadExchangeRatesTool, uploadReceiptImageTool as sapConcurUploadReceiptImageTool, } from '@/tools/sap_concur' import { @@ -5463,7 +5463,7 @@ export const tools: Record = { sap_concur_get_allocation: sapConcurGetAllocationTool, sap_concur_get_budget: sapConcurGetBudgetTool, sap_concur_get_cash_advance: sapConcurGetCashAdvanceTool, - sap_concur_get_exchange_rate: sapConcurGetExchangeRateTool, + sap_concur_upload_exchange_rates: sapConcurUploadExchangeRatesTool, sap_concur_get_expected_expense: sapConcurGetExpectedExpenseTool, sap_concur_get_expense: sapConcurGetExpenseTool, sap_concur_get_expense_report: sapConcurGetExpenseReportTool, diff --git a/apps/sim/tools/sap_concur/approve_expense_report.ts b/apps/sim/tools/sap_concur/approve_expense_report.ts index 5320989be85..9ab472482d1 100644 --- a/apps/sim/tools/sap_concur/approve_expense_report.ts +++ b/apps/sim/tools/sap_concur/approve_expense_report.ts @@ -27,7 +27,7 @@ export const approveExpenseReportTool: ToolConfig< type: 'string', required: false, visibility: 'user-only', - description: 'OAuth grant type: client_credentials (default), password, refresh_token', + description: 'OAuth grant type: client_credentials (default) or password', }, clientId: { type: 'string', diff --git a/apps/sim/tools/sap_concur/associate_attendees.ts b/apps/sim/tools/sap_concur/associate_attendees.ts index c452dce4e09..2efd05707c6 100644 --- a/apps/sim/tools/sap_concur/associate_attendees.ts +++ b/apps/sim/tools/sap_concur/associate_attendees.ts @@ -25,7 +25,7 @@ export const associateAttendeesTool: ToolConfig type: 'string', required: false, visibility: 'user-only', - description: 'OAuth grant type: client_credentials (default), password, refresh_token', + description: 'OAuth grant type: client_credentials (default) or password', }, clientId: { type: 'string', diff --git a/apps/sim/tools/sap_concur/get_cash_advance.ts b/apps/sim/tools/sap_concur/get_cash_advance.ts index 49e0e71f7b2..9ef40958596 100644 --- a/apps/sim/tools/sap_concur/get_cash_advance.ts +++ b/apps/sim/tools/sap_concur/get_cash_advance.ts @@ -23,7 +23,7 @@ export const getCashAdvanceTool: ToolConfig = { type: 'string', required: false, visibility: 'user-only', - description: 'OAuth grant type: client_credentials (default), password, refresh_token', + description: 'OAuth grant type: client_credentials (default) or password', }, clientId: { type: 'string', diff --git a/apps/sim/tools/sap_concur/get_list_item.ts b/apps/sim/tools/sap_concur/get_list_item.ts index 611844a8bbc..80ed6af505c 100644 --- a/apps/sim/tools/sap_concur/get_list_item.ts +++ b/apps/sim/tools/sap_concur/get_list_item.ts @@ -23,7 +23,7 @@ export const getListItemTool: ToolConfig = { type: 'string', required: false, visibility: 'user-only', - description: 'OAuth grant type: client_credentials (default), password, refresh_token', + description: 'OAuth grant type: client_credentials (default) or password', }, clientId: { type: 'string', diff --git a/apps/sim/tools/sap_concur/index.ts b/apps/sim/tools/sap_concur/index.ts index 621496eebe5..65b26376efa 100644 --- a/apps/sim/tools/sap_concur/index.ts +++ b/apps/sim/tools/sap_concur/index.ts @@ -19,7 +19,6 @@ export { deleteUserTool } from '@/tools/sap_concur/delete_user' export { getAllocationTool } from '@/tools/sap_concur/get_allocation' export { getBudgetTool } from '@/tools/sap_concur/get_budget' export { getCashAdvanceTool } from '@/tools/sap_concur/get_cash_advance' -export { getExchangeRateTool } from '@/tools/sap_concur/get_exchange_rate' export { getExpectedExpenseTool } from '@/tools/sap_concur/get_expected_expense' export { getExpenseTool } from '@/tools/sap_concur/get_expense' export { getExpenseReportTool } from '@/tools/sap_concur/get_expense_report' @@ -67,4 +66,5 @@ export { updateExpenseReportTool } from '@/tools/sap_concur/update_expense_repor export { updateListItemTool } from '@/tools/sap_concur/update_list_item' export { updateTravelRequestTool } from '@/tools/sap_concur/update_travel_request' export { updateUserTool } from '@/tools/sap_concur/update_user' +export { uploadExchangeRatesTool } from '@/tools/sap_concur/upload_exchange_rates' export { uploadReceiptImageTool } from '@/tools/sap_concur/upload_receipt_image' diff --git a/apps/sim/tools/sap_concur/issue_cash_advance.ts b/apps/sim/tools/sap_concur/issue_cash_advance.ts index 527edaf9a7c..4e11a53faf9 100644 --- a/apps/sim/tools/sap_concur/issue_cash_advance.ts +++ b/apps/sim/tools/sap_concur/issue_cash_advance.ts @@ -23,7 +23,7 @@ export const issueCashAdvanceTool: ToolConfig type: 'string', required: false, visibility: 'user-only', - description: 'OAuth grant type: client_credentials (default), password, refresh_token', + description: 'OAuth grant type: client_credentials (default) or password', }, clientId: { type: 'string', diff --git a/apps/sim/tools/sap_concur/list_receipts.ts b/apps/sim/tools/sap_concur/list_receipts.ts index 09ed01b302e..c33d6c896b1 100644 --- a/apps/sim/tools/sap_concur/list_receipts.ts +++ b/apps/sim/tools/sap_concur/list_receipts.ts @@ -23,7 +23,7 @@ export const listReceiptsTool: ToolConfig type: 'string', required: false, visibility: 'user-only', - description: 'OAuth grant type: client_credentials (default), password, refresh_token', + description: 'OAuth grant type: client_credentials (default) or password', }, clientId: { type: 'string', diff --git a/apps/sim/tools/sap_concur/move_travel_request.ts b/apps/sim/tools/sap_concur/move_travel_request.ts index 0e8ed0016c4..346fc7dd7a4 100644 --- a/apps/sim/tools/sap_concur/move_travel_request.ts +++ b/apps/sim/tools/sap_concur/move_travel_request.ts @@ -24,7 +24,7 @@ export const moveTravelRequestTool: ToolConfig | string } diff --git a/apps/sim/tools/sap_concur/update_allocation.ts b/apps/sim/tools/sap_concur/update_allocation.ts index d5dfeabad74..c7759a430b7 100644 --- a/apps/sim/tools/sap_concur/update_allocation.ts +++ b/apps/sim/tools/sap_concur/update_allocation.ts @@ -24,7 +24,7 @@ export const updateAllocationTool: ToolConfig = { - id: 'sap_concur_get_exchange_rate', - name: 'SAP Concur Get Exchange Rate', +export const uploadExchangeRatesTool: ToolConfig< + UploadExchangeRatesParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_upload_exchange_rates', + name: 'SAP Concur Upload Exchange Rates', description: 'Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body: { currency_sets: [{ from_crn_code, to_crn_code, start_date: "YYYY-MM-DD", rate }] }', version: '1.0.0', @@ -23,7 +26,7 @@ export const getExchangeRateTool: ToolConfig Date: Wed, 6 May 2026 18:31:11 -0700 Subject: [PATCH 05/13] finished --- apps/docs/content/docs/en/tools/sap_concur.mdx | 2 +- apps/sim/app/(landing)/integrations/data/integrations.json | 4 ++-- apps/sim/tools/sap_concur/upload_exchange_rates.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/docs/content/docs/en/tools/sap_concur.mdx b/apps/docs/content/docs/en/tools/sap_concur.mdx index 4da75a6d362..3f03b025a86 100644 --- a/apps/docs/content/docs/en/tools/sap_concur.mdx +++ b/apps/docs/content/docs/en/tools/sap_concur.mdx @@ -735,7 +735,7 @@ Get a cash advance (GET /cashadvance/v4/cashadvances/{cashadvanceId}). ### `sap_concur_upload_exchange_rates` -Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body: { currency_sets: [{ from_crn_code, to_crn_code, start_date: +Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body contains a currency_sets array, each with from_crn_code, to_crn_code, start_date (YYYY-MM-DD), and rate. #### Input diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 7a4810b2d60..7aa5a1d235a 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -11749,8 +11749,8 @@ "description": "List budget categories (GET /budget/v4/budgetCategory)." }, { - "name": "Get Exchange Rate", - "description": "Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body: { currency_sets: [{ from_crn_code, to_crn_code, start_date: " + "name": "Upload Exchange Rates", + "description": "Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body contains a currency_sets array, each with from_crn_code, to_crn_code, start_date (YYYY-MM-DD), and rate." }, { "name": "Create Purchase Request", diff --git a/apps/sim/tools/sap_concur/upload_exchange_rates.ts b/apps/sim/tools/sap_concur/upload_exchange_rates.ts index 31441ff0b45..7241da859af 100644 --- a/apps/sim/tools/sap_concur/upload_exchange_rates.ts +++ b/apps/sim/tools/sap_concur/upload_exchange_rates.ts @@ -13,7 +13,7 @@ export const uploadExchangeRatesTool: ToolConfig< id: 'sap_concur_upload_exchange_rates', name: 'SAP Concur Upload Exchange Rates', description: - 'Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body: { currency_sets: [{ from_crn_code, to_crn_code, start_date: "YYYY-MM-DD", rate }] }', + 'Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body contains a currency_sets array, each with from_crn_code, to_crn_code, start_date (YYYY-MM-DD), and rate.', version: '1.0.0', params: { datacenter: { From 79c6fdea765763c036fa31ae383665e0e1e69bb3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 18:37:50 -0700 Subject: [PATCH 06/13] docs --- apps/docs/content/docs/en/tools/sap_concur.mdx | 2 +- apps/sim/app/(landing)/integrations/data/integrations.json | 2 +- apps/sim/tools/sap_concur/list_expense_reports.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/docs/content/docs/en/tools/sap_concur.mdx b/apps/docs/content/docs/en/tools/sap_concur.mdx index 3f03b025a86..0dbd8639d6a 100644 --- a/apps/docs/content/docs/en/tools/sap_concur.mdx +++ b/apps/docs/content/docs/en/tools/sap_concur.mdx @@ -1693,7 +1693,7 @@ List expenses on a report (GET /expensereports/v4/users/{userId}/context/{contex ### `sap_concur_list_expense_reports` -List expense reports (GET /api/v3.0/expense/reports). Returns a v3 envelope { Items, NextPage }. +List expense reports (GET /api/v3.0/expense/reports). Returns a v3 envelope with Items and NextPage. #### Input diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 7aa5a1d235a..84492a6464e 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -11494,7 +11494,7 @@ "operations": [ { "name": "List Expense Reports", - "description": "List expense reports (GET /api/v3.0/expense/reports). Returns a v3 envelope { Items, NextPage }." + "description": "List expense reports (GET /api/v3.0/expense/reports). Returns a v3 envelope with Items and NextPage." }, { "name": "Get Expense Report", diff --git a/apps/sim/tools/sap_concur/list_expense_reports.ts b/apps/sim/tools/sap_concur/list_expense_reports.ts index 15227f31c0a..b2293bf1eaf 100644 --- a/apps/sim/tools/sap_concur/list_expense_reports.ts +++ b/apps/sim/tools/sap_concur/list_expense_reports.ts @@ -11,7 +11,7 @@ export const listExpenseReportsTool: ToolConfig Date: Wed, 6 May 2026 18:50:07 -0700 Subject: [PATCH 07/13] fix(docs): escape braces in tool/trigger description prose for MDX Tool and trigger descriptions can contain URL path placeholders like {reportId} or JSON-shape hints like { Items, NextPage }. When rendered as MDX prose (not table cells), these were emitted unescaped and MDX parsed them as JSX expressions, failing prerender with "ReferenceError: reportId is not defined". Escape { and } in the operation-level description and trigger description renderers, matching the existing escaping in table-cell descriptions. Co-Authored-By: Claude Opus 4.7 --- .../docs/content/docs/en/tools/sap_concur.mdx | 106 +++++++++--------- scripts/generate-docs.ts | 10 +- 2 files changed, 61 insertions(+), 55 deletions(-) diff --git a/apps/docs/content/docs/en/tools/sap_concur.mdx b/apps/docs/content/docs/en/tools/sap_concur.mdx index 0dbd8639d6a..301c14d9371 100644 --- a/apps/docs/content/docs/en/tools/sap_concur.mdx +++ b/apps/docs/content/docs/en/tools/sap_concur.mdx @@ -42,7 +42,7 @@ Connect SAP Concur via OAuth 2.0. Manage expense reports and line items, allocat ### `sap_concur_approve_expense_report` -Approve an expense report as a manager (PATCH /expensereports/v4/reports/{reportId}/approve). Required body field: comment. +Approve an expense report as a manager (PATCH /expensereports/v4/reports/\{reportId\}/approve). Required body field: comment. #### Input @@ -67,7 +67,7 @@ Approve an expense report as a manager (PATCH /expensereports/v4/reports/{report ### `sap_concur_associate_attendees` -Associate attendees with an expense (POST /expensereports/v4/users/{userId}/context/TRAVELER/reports/{reportId}/expenses/{expenseId}/attendees). +Associate attendees with an expense (POST /expensereports/v4/users/\{userId\}/context/TRAVELER/reports/\{reportId\}/expenses/\{expenseId\}/attendees). #### Input @@ -121,7 +121,7 @@ Create a cash advance (POST /cashadvance/v4/cashadvances). ### `sap_concur_create_expected_expense` -Create an expected expense on a travel request (POST /travelrequest/v4/requests/{requestUuid}/expenses). +Create an expected expense on a travel request (POST /travelrequest/v4/requests/\{requestUuid\}/expenses). #### Input @@ -162,7 +162,7 @@ Create an expected expense on a travel request (POST /travelrequest/v4/requests/ ### `sap_concur_create_expense_report` -Create an expense report (POST /expensereports/v4/users/{userId}/context/{contextType}/reports — supported contexts: TRAVELER, PROXY). Required body fields: name, policyId. +Create an expense report (POST /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports — supported contexts: TRAVELER, PROXY). Required body fields: name, policyId. #### Input @@ -253,7 +253,7 @@ Create a purchase request (POST /purchaserequest/v4/purchaserequests). ### `sap_concur_create_quick_expense` -Create a quick expense (POST /quickexpense/v4/users/{userId}/context/TRAVELER/quickexpenses). +Create a quick expense (POST /quickexpense/v4/users/\{userId\}/context/TRAVELER/quickexpenses). #### Input @@ -280,7 +280,7 @@ Create a quick expense (POST /quickexpense/v4/users/{userId}/context/TRAVELER/qu ### `sap_concur_create_quick_expense_with_image` -Create a quick expense with an attached image (POST /quickexpense/v4/users/{userId}/context/{contextType}/quickexpenses/image). +Create a quick expense with an attached image (POST /quickexpense/v4/users/\{userId\}/context/\{contextType\}/quickexpenses/image). #### Input @@ -308,7 +308,7 @@ Create a quick expense with an attached image (POST /quickexpense/v4/users/{user ### `sap_concur_create_report_comment` -Create a comment on a report (POST /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/comments). +Create a comment on a report (POST /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/comments). #### Input @@ -447,7 +447,7 @@ Create a new user identity (POST /profile/identity/v4.1/Users). ### `sap_concur_delete_expected_expense` -Delete an expected expense (DELETE /travelrequest/v4/expenses/{expenseUuid}). +Delete an expected expense (DELETE /travelrequest/v4/expenses/\{expenseUuid\}). #### Input @@ -472,7 +472,7 @@ Delete an expected expense (DELETE /travelrequest/v4/expenses/{expenseUuid}). ### `sap_concur_delete_expense` -Delete an expense (DELETE /expensereports/v4/reports/{reportId}/expenses/{expenseId}). +Delete an expense (DELETE /expensereports/v4/reports/\{reportId\}/expenses/\{expenseId\}). #### Input @@ -497,7 +497,7 @@ Delete an expense (DELETE /expensereports/v4/reports/{reportId}/expenses/{expens ### `sap_concur_delete_expense_report` -Delete an expense report (DELETE /expensereports/v4/reports/{reportId}). +Delete an expense report (DELETE /expensereports/v4/reports/\{reportId\}). #### Input @@ -521,7 +521,7 @@ Delete an expense report (DELETE /expensereports/v4/reports/{reportId}). ### `sap_concur_delete_list_item` -Delete a list item (DELETE /list/v4/items/{itemId}). +Delete a list item (DELETE /list/v4/items/\{itemId\}). #### Input @@ -545,7 +545,7 @@ Delete a list item (DELETE /list/v4/items/{itemId}). ### `sap_concur_delete_travel_request` -Delete a travel request (DELETE /travelrequest/v4/requests/{requestUuid}). +Delete a travel request (DELETE /travelrequest/v4/requests/\{requestUuid\}). #### Input @@ -570,7 +570,7 @@ Delete a travel request (DELETE /travelrequest/v4/requests/{requestUuid}). ### `sap_concur_delete_user` -Delete a user identity (DELETE /profile/identity/v4.1/Users/{id}). +Delete a user identity (DELETE /profile/identity/v4.1/Users/\{id\}). #### Input @@ -594,7 +594,7 @@ Delete a user identity (DELETE /profile/identity/v4.1/Users/{id}). ### `sap_concur_get_allocation` -Get a single allocation (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/allocations/{allocationId}). +Get a single allocation (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/allocations/\{allocationId\}). #### Input @@ -638,7 +638,7 @@ Get a single allocation (GET /expensereports/v4/users/{userId}/context/{contextT ### `sap_concur_get_budget` -Get a budget item header by ID (GET /budget/v4/budgetItemHeader/{id}). +Get a budget item header by ID (GET /budget/v4/budgetItemHeader/\{id\}). #### Input @@ -686,7 +686,7 @@ Get a budget item header by ID (GET /budget/v4/budgetItemHeader/{id}). ### `sap_concur_get_cash_advance` -Get a cash advance (GET /cashadvance/v4/cashadvances/{cashadvanceId}). +Get a cash advance (GET /cashadvance/v4/cashadvances/\{cashadvanceId\}). #### Input @@ -762,7 +762,7 @@ Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body ### `sap_concur_get_expected_expense` -Get an expected expense (GET /travelrequest/v4/expenses/{expenseUuid}). +Get an expected expense (GET /travelrequest/v4/expenses/\{expenseUuid\}). #### Input @@ -802,7 +802,7 @@ Get an expected expense (GET /travelrequest/v4/expenses/{expenseUuid}). ### `sap_concur_get_expense` -Get a single expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}). +Get a single expense (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses/\{expenseId\}). #### Input @@ -880,7 +880,7 @@ Get a single expense (GET /expensereports/v4/users/{userId}/context/{contextType ### `sap_concur_get_expense_report` -Retrieve a single expense report header by id via Expense Report v4 (/expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}). +Retrieve a single expense report header by id via Expense Report v4 (/expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}). #### Input @@ -974,7 +974,7 @@ Retrieve a single expense report header by id via Expense Report v4 (/expenserep ### `sap_concur_get_itemizations` -Get expense itemizations (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/itemizations). +Get expense itemizations (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses/\{expenseId\}/itemizations). #### Input @@ -1022,7 +1022,7 @@ Get expense itemizations (GET /expensereports/v4/users/{userId}/context/{context ### `sap_concur_get_itinerary` -Get a single trip/itinerary (GET /api/travel/trip/v1.1/{tripID}). +Get a single trip/itinerary (GET /api/travel/trip/v1.1/\{tripID\}). #### Input @@ -1072,7 +1072,7 @@ Get a single trip/itinerary (GET /api/travel/trip/v1.1/{tripID}). ### `sap_concur_get_list` -Get a single custom list (GET /list/v4/lists/{listId}). +Get a single custom list (GET /list/v4/lists/\{listId\}). #### Input @@ -1108,7 +1108,7 @@ Get a single custom list (GET /list/v4/lists/{listId}). ### `sap_concur_get_list_item` -Get a single list item (GET /list/v4/items/{itemId}). +Get a single list item (GET /list/v4/items/\{itemId\}). #### Input @@ -1142,7 +1142,7 @@ Get a single list item (GET /list/v4/items/{itemId}). ### `sap_concur_get_purchase_request` -Get a purchase request by ID (GET /purchaserequest/v4/purchaserequests/{id}). +Get a purchase request by ID (GET /purchaserequest/v4/purchaserequests/\{id\}). #### Input @@ -1178,7 +1178,7 @@ Get a purchase request by ID (GET /purchaserequest/v4/purchaserequests/{id}). ### `sap_concur_get_receipt` -Get a single receipt by ID (GET /receipts/v4/{receiptId}). +Get a single receipt by ID (GET /receipts/v4/\{receiptId\}). #### Input @@ -1210,7 +1210,7 @@ Get a single receipt by ID (GET /receipts/v4/{receiptId}). ### `sap_concur_get_receipt_status` -Get receipt processing status (GET /receipts/v4/status/{receiptId}). +Get receipt processing status (GET /receipts/v4/status/\{receiptId\}). #### Input @@ -1290,7 +1290,7 @@ Get a travel profile (GET /api/travelprofile/v2.0/profile). Returns the calling ### `sap_concur_get_travel_request` -Get a single travel request (GET /travelrequest/v4/requests/{requestUuid}). +Get a single travel request (GET /travelrequest/v4/requests/\{requestUuid\}). #### Input @@ -1400,7 +1400,7 @@ Get a single travel request (GET /travelrequest/v4/requests/{requestUuid}). ### `sap_concur_get_user` -Get a single user by UUID (GET /profile/identity/v4.1/Users/{id}). +Get a single user by UUID (GET /profile/identity/v4.1/Users/\{id\}). #### Input @@ -1426,7 +1426,7 @@ Get a single user by UUID (GET /profile/identity/v4.1/Users/{id}). ### `sap_concur_issue_cash_advance` -Issue a cash advance (POST /cashadvance/v4/cashadvances/{cashadvanceId}/issue). +Issue a cash advance (POST /cashadvance/v4/cashadvances/\{cashadvanceId\}/issue). #### Input @@ -1455,7 +1455,7 @@ Issue a cash advance (POST /cashadvance/v4/cashadvances/{cashadvanceId}/issue). ### `sap_concur_list_allocations` -List allocations on an expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/allocations). +List allocations on an expense (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses/\{expenseId\}/allocations). #### Input @@ -1482,7 +1482,7 @@ List allocations on an expense (GET /expensereports/v4/users/{userId}/context/{c ### `sap_concur_list_attendee_associations` -List attendees associated with an expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/attendees). +List attendees associated with an expense (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses/\{expenseId\}/attendees). #### Input @@ -1579,7 +1579,7 @@ List budget item headers (GET /budget/v4/budgetItemHeader). ### `sap_concur_list_exceptions` -List exceptions on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/exceptions). +List exceptions on a report (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/exceptions). #### Input @@ -1612,7 +1612,7 @@ List exceptions on a report (GET /expensereports/v4/users/{userId}/context/{cont ### `sap_concur_list_expected_expenses` -List expected expenses on a travel request (GET /travelrequest/v4/requests/{requestUuid}/expenses). +List expected expenses on a travel request (GET /travelrequest/v4/requests/\{requestUuid\}/expenses). #### Input @@ -1637,7 +1637,7 @@ List expected expenses on a travel request (GET /travelrequest/v4/requests/{requ ### `sap_concur_list_expenses` -List expenses on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses). +List expenses on a report (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses). #### Input @@ -1868,7 +1868,7 @@ List custom lists (GET /list/v4/lists). ### `sap_concur_list_list_items` -List the top-level items (children) for a custom list (GET /list/v4/lists/{listId}/children). +List the top-level items (children) for a custom list (GET /list/v4/lists/\{listId\}/children). #### Input @@ -1919,7 +1919,7 @@ List the top-level items (children) for a custom list (GET /list/v4/lists/{listI ### `sap_concur_list_receipts` -List receipts for a user (GET /receipts/v4/users/{userId}). +List receipts for a user (GET /receipts/v4/users/\{userId\}). #### Input @@ -1951,7 +1951,7 @@ List receipts for a user (GET /receipts/v4/users/{userId}). ### `sap_concur_list_report_comments` -List comments on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/comments). +List comments on a report (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/comments). #### Input @@ -1991,7 +1991,7 @@ List comments on a report (GET /expensereports/v4/users/{userId}/context/{contex ### `sap_concur_list_reports_to_approve` -List expense reports awaiting approval (GET /expensereports/v4/users/{userId}/context/MANAGER/reportsToApprove). +List expense reports awaiting approval (GET /expensereports/v4/users/\{userId\}/context/MANAGER/reportsToApprove). #### Input @@ -2032,7 +2032,7 @@ List expense reports awaiting approval (GET /expensereports/v4/users/{userId}/co ### `sap_concur_get_request_cash_advance` -Get a single cash advance assigned to a travel request (GET /travelrequest/v4/cashadvances/{cashAdvanceUuid}). +Get a single cash advance assigned to a travel request (GET /travelrequest/v4/cashadvances/\{cashAdvanceUuid\}). #### Input @@ -2114,7 +2114,7 @@ List travel profile summaries (GET /api/travelprofile/v2.0/summary). LastModifie ### `sap_concur_list_travel_request_comments` -List comments on a travel request (GET /travelrequest/v4/requests/{requestUuid}/comments). +List comments on a travel request (GET /travelrequest/v4/requests/\{requestUuid\}/comments). #### Input @@ -2250,7 +2250,7 @@ List Concur user identities (GET /profile/identity/v4.1/Users). ### `sap_concur_move_travel_request` -Move a travel request through workflow (POST /travelrequest/v4/requests/{requestUuid}/{action}). Valid actions: submit, recall, cancel, approve, sendback, close, reopen. +Move a travel request through workflow (POST /travelrequest/v4/requests/\{requestUuid\}/\{action\}). Valid actions: submit, recall, cancel, approve, sendback, close, reopen. #### Input @@ -2291,7 +2291,7 @@ Move a travel request through workflow (POST /travelrequest/v4/requests/{request ### `sap_concur_recall_expense_report` -Recall a submitted expense report (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/recall — supported contexts: TRAVELER, PROXY). No request body is required. +Recall a submitted expense report (PATCH /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/recall — supported contexts: TRAVELER, PROXY). No request body is required. #### Input @@ -2318,7 +2318,7 @@ Recall a submitted expense report (PATCH /expensereports/v4/users/{userId}/conte ### `sap_concur_remove_all_attendees` -Remove all attendees from an expense (DELETE /expensereports/v4/users/{userId}/context/TRAVELER/reports/{reportId}/expenses/{expenseId}/attendees). +Remove all attendees from an expense (DELETE /expensereports/v4/users/\{userId\}/context/TRAVELER/reports/\{reportId\}/expenses/\{expenseId\}/attendees). #### Input @@ -2425,7 +2425,7 @@ Search users via SCIM .search endpoint (POST /profile/identity/v4.1/Users/.searc ### `sap_concur_send_back_expense_report` -Send back an expense report to the employee (PATCH /expensereports/v4/reports/{reportId}/sendBack). Required body field: comment. +Send back an expense report to the employee (PATCH /expensereports/v4/reports/\{reportId\}/sendBack). Required body field: comment. #### Input @@ -2450,7 +2450,7 @@ Send back an expense report to the employee (PATCH /expensereports/v4/reports/{r ### `sap_concur_submit_expense_report` -Submit an expense report into the workflow via Expense Report v4 (PATCH /expensereports/v4/users/{userId}/reports/{reportId}/submit). +Submit an expense report into the workflow via Expense Report v4 (PATCH /expensereports/v4/users/\{userId\}/reports/\{reportId\}/submit). #### Input @@ -2476,7 +2476,7 @@ Submit an expense report into the workflow via Expense Report v4 (PATCH /expense ### `sap_concur_update_allocation` -Update an allocation (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/allocations/{allocationId}). +Update an allocation (PATCH /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/allocations/\{allocationId\}). #### Input @@ -2504,7 +2504,7 @@ Update an allocation (PATCH /expensereports/v4/users/{userId}/context/{contextTy ### `sap_concur_update_expected_expense` -Update an expected expense (PUT /travelrequest/v4/expenses/{expenseUuid}). +Update an expected expense (PUT /travelrequest/v4/expenses/\{expenseUuid\}). #### Input @@ -2545,7 +2545,7 @@ Update an expected expense (PUT /travelrequest/v4/expenses/{expenseUuid}). ### `sap_concur_update_expense` -Update an expense (PATCH /expensereports/v4/reports/{reportId}/expenses/{expenseId}). +Update an expense (PATCH /expensereports/v4/reports/\{reportId\}/expenses/\{expenseId\}). #### Input @@ -2571,7 +2571,7 @@ Update an expense (PATCH /expensereports/v4/reports/{reportId}/expenses/{expense ### `sap_concur_update_expense_report` -Update an unsubmitted expense report (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId} — supported contexts: TRAVELER, PROXY). Body fields: businessPurpose, comment, customData, name, etc. +Update an unsubmitted expense report (PATCH /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\} — supported contexts: TRAVELER, PROXY). Body fields: businessPurpose, comment, customData, name, etc. #### Input @@ -2598,7 +2598,7 @@ Update an unsubmitted expense report (PATCH /expensereports/v4/users/{userId}/co ### `sap_concur_update_list_item` -Update a list item (PUT /list/v4/items/{itemId}). +Update a list item (PUT /list/v4/items/\{itemId\}). #### Input @@ -2633,7 +2633,7 @@ Update a list item (PUT /list/v4/items/{itemId}). ### `sap_concur_update_travel_request` -Update a travel request (PUT /travelrequest/v4/requests/{requestUuid}). +Update a travel request (PUT /travelrequest/v4/requests/\{requestUuid\}). #### Input @@ -2708,7 +2708,7 @@ Update a travel request (PUT /travelrequest/v4/requests/{requestUuid}). ### `sap_concur_update_user` -Patch a user identity (PATCH /profile/identity/v4.1/Users/{id}). +Patch a user identity (PATCH /profile/identity/v4.1/Users/\{id\}). #### Input @@ -2733,7 +2733,7 @@ Patch a user identity (PATCH /profile/identity/v4.1/Users/{id}). ### `sap_concur_upload_receipt_image` -Upload an image-only receipt (POST /receipts/v4/users/{userId}/image-only-receipts). +Upload an image-only receipt (POST /receipts/v4/users/\{userId\}/image-only-receipts). #### Input diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 93b794d8762..65d93e3e393 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -2711,7 +2711,10 @@ async function generateMarkdownForBlock( if (toolInfo) { if (toolInfo.description && toolInfo.description !== 'No description available') { - toolsSection += `${toolInfo.description}\n\n` + const escapedToolDescription = toolInfo.description + .replace(/\{/g, '\\{') + .replace(/\}/g, '\\}') + toolsSection += `${escapedToolDescription}\n\n` } toolsSection += '#### Input\n\n' @@ -3552,7 +3555,10 @@ function generateTriggerProviderDoc( const separator = i < triggers.length - 1 ? '\n---\n\n' : '' triggersSection += `### ${trigger.name}\n\n` - triggersSection += `${trigger.description}\n\n` + const escapedTriggerDescription = trigger.description + .replace(/\{/g, '\\{') + .replace(/\}/g, '\\}') + triggersSection += `${escapedTriggerDescription}\n\n` triggersSection += configSection triggersSection += outputSection triggersSection += separator From b74fc86f80ab70cb6bb4bd183c2f54f1c7d86480 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 19:17:56 -0700 Subject: [PATCH 08/13] fix(sap-concur): align with live API on travel-profile, itineraries, and context types - list_travel_profiles_summary: rename Status query to Active with 1/0 values, tighten LastModifiedDate format hint - list_itineraries / get_itinerary: use documented userid_type / userid_value / ItemsPerPage / Page query keys - create_report_comment: contextType allows MANAGER (move to EXPENSE_READ_CONTEXT_TYPE_OPS) - get_list_item: drop unused listId from block (tool only needs itemId) - Tighten description copy on list_expenses/get_itemizations/associate_attendees/remove_all_attendees Co-Authored-By: Claude Opus 4.7 --- apps/sim/blocks/blocks/sap_concur.ts | 18 ++++++++++-------- .../tools/sap_concur/associate_attendees.ts | 2 +- apps/sim/tools/sap_concur/get_itemizations.ts | 2 +- apps/sim/tools/sap_concur/get_itinerary.ts | 4 ++-- apps/sim/tools/sap_concur/list_expenses.ts | 2 +- apps/sim/tools/sap_concur/list_itineraries.ts | 8 ++++---- .../sap_concur/list_travel_profiles_summary.ts | 6 +++--- .../tools/sap_concur/remove_all_attendees.ts | 2 +- 8 files changed, 23 insertions(+), 21 deletions(-) diff --git a/apps/sim/blocks/blocks/sap_concur.ts b/apps/sim/blocks/blocks/sap_concur.ts index b67e2178f43..f7e83b6bfb4 100644 --- a/apps/sim/blocks/blocks/sap_concur.ts +++ b/apps/sim/blocks/blocks/sap_concur.ts @@ -44,6 +44,7 @@ const EXPENSE_READ_CONTEXT_TYPE_OPS = [ 'sap_concur_list_expenses', 'sap_concur_get_expense', 'sap_concur_get_itemizations', + 'sap_concur_create_report_comment', 'sap_concur_list_report_comments', 'sap_concur_list_exceptions', ] @@ -59,7 +60,6 @@ const ATTENDEE_CONTEXT_TYPE_OPS = [ 'sap_concur_list_attendee_associations', 'sap_concur_associate_attendees', 'sap_concur_remove_all_attendees', - 'sap_concur_create_report_comment', ] const ALLOCATION_CONTEXT_TYPE_OPS = [ @@ -928,7 +928,7 @@ export const SapConcurBlock: BlockConfig = { placeholder: 'List ID', condition: { field: 'operation', - value: ['sap_concur_get_list', 'sap_concur_list_list_items', 'sap_concur_get_list_item'], + value: ['sap_concur_get_list', 'sap_concur_list_list_items'], }, required: { field: 'operation', @@ -1080,7 +1080,7 @@ export const SapConcurBlock: BlockConfig = { id: 'lastModifiedDate', title: 'Last Modified Date', type: 'short-input', - placeholder: 'YYYY-MM-DD or 1900-01-01T12:00:00', + placeholder: '1900-01-01T00:00:00 (UTC datetime)', condition: { field: 'operation', value: 'sap_concur_list_travel_profiles_summary', @@ -1110,8 +1110,8 @@ export const SapConcurBlock: BlockConfig = { type: 'dropdown', options: [ { label: 'Any', id: '' }, - { label: 'Active', id: 'Active' }, - { label: 'Inactive', id: 'Inactive' }, + { label: 'Active', id: '1' }, + { label: 'Inactive', id: '0' }, ], value: () => '', condition: { field: 'operation', value: 'sap_concur_list_travel_profiles_summary' }, @@ -1684,7 +1684,6 @@ export const SapConcurBlock: BlockConfig = { case 'sap_concur_get_list_item': return { ...auth, - listId: params.listId || undefined, itemId: params.itemId, } case 'sap_concur_list_budgets': @@ -1723,7 +1722,7 @@ export const SapConcurBlock: BlockConfig = { page: params.travelProfilePage ? Number(params.travelProfilePage) : undefined, itemsPerPage: params.itemsPerPage ? Number(params.itemsPerPage) : undefined, active: - params.travelProfileActive === 'Active' || params.travelProfileActive === 'Inactive' + params.travelProfileActive === '1' || params.travelProfileActive === '0' ? params.travelProfileActive : undefined, travelConfigs: params.travelConfigs || undefined, @@ -1886,7 +1885,10 @@ export const SapConcurBlock: BlockConfig = { page: { type: 'number', description: 'Page number (lists/list_items)' }, travelProfilePage: { type: 'number', description: 'Profile summary page number' }, itemsPerPage: { type: 'number', description: 'Profile summary items per page' }, - travelProfileActive: { type: 'string', description: 'Status filter ("Active" or "Inactive")' }, + travelProfileActive: { + type: 'string', + description: 'Active filter ("1" for active, "0" for inactive)', + }, travelConfigs: { type: 'string', description: 'Comma-separated travel config ids' }, searchText: { type: 'string', description: 'Locations v5 free-text search' }, locCode: { type: 'string', description: 'Locations v5 location code' }, diff --git a/apps/sim/tools/sap_concur/associate_attendees.ts b/apps/sim/tools/sap_concur/associate_attendees.ts index 2efd05707c6..ae75a711c16 100644 --- a/apps/sim/tools/sap_concur/associate_attendees.ts +++ b/apps/sim/tools/sap_concur/associate_attendees.ts @@ -12,7 +12,7 @@ export const associateAttendeesTool: ToolConfig { const tripId = trimRequired(params.tripId, 'tripId') const query = buildListQuery({ - useridType: params.useridType, - useridValue: params.useridValue, + userid_type: params.useridType, + userid_value: params.useridValue, systemFormat: params.systemFormat, }) return { diff --git a/apps/sim/tools/sap_concur/list_expenses.ts b/apps/sim/tools/sap_concur/list_expenses.ts index 744973df63c..8c90cd77495 100644 --- a/apps/sim/tools/sap_concur/list_expenses.ts +++ b/apps/sim/tools/sap_concur/list_expenses.ts @@ -66,7 +66,7 @@ export const listExpensesTool: ToolConfig Date: Wed, 6 May 2026 19:21:15 -0700 Subject: [PATCH 09/13] fix(sap-concur): correct Cash Advance v4.1 paths, add SCIM filter param - Update Cash Advance create/get/issue tools from /cashadvance/v4/ to /cashadvance/v4.1/ to match the live API - Add filter query param to list_users (SCIM v4.1 supports filtering by userName, employeeNumber, externalId) - Regenerate docs MDX Co-Authored-By: Claude Opus 4.7 --- .../docs/content/docs/en/tools/sap_concur.mdx | 19 ++++++++++--------- .../integrations/data/integrations.json | 10 +++++----- .../tools/sap_concur/create_cash_advance.ts | 4 ++-- apps/sim/tools/sap_concur/get_cash_advance.ts | 4 ++-- .../tools/sap_concur/issue_cash_advance.ts | 4 ++-- apps/sim/tools/sap_concur/list_users.ts | 8 ++++++++ apps/sim/tools/sap_concur/types.ts | 1 + 7 files changed, 30 insertions(+), 20 deletions(-) diff --git a/apps/docs/content/docs/en/tools/sap_concur.mdx b/apps/docs/content/docs/en/tools/sap_concur.mdx index 301c14d9371..9abd0891879 100644 --- a/apps/docs/content/docs/en/tools/sap_concur.mdx +++ b/apps/docs/content/docs/en/tools/sap_concur.mdx @@ -67,7 +67,7 @@ Approve an expense report as a manager (PATCH /expensereports/v4/reports/\{repor ### `sap_concur_associate_attendees` -Associate attendees with an expense (POST /expensereports/v4/users/\{userId\}/context/TRAVELER/reports/\{reportId\}/expenses/\{expenseId\}/attendees). +Associate attendees with an expense (POST /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses/\{expenseId\}/attendees). #### Input @@ -96,7 +96,7 @@ Associate attendees with an expense (POST /expensereports/v4/users/\{userId\}/co ### `sap_concur_create_cash_advance` -Create a cash advance (POST /cashadvance/v4/cashadvances). +Create a cash advance (POST /cashadvance/v4.1/cashadvances). #### Input @@ -686,7 +686,7 @@ Get a budget item header by ID (GET /budget/v4/budgetItemHeader/\{id\}). ### `sap_concur_get_cash_advance` -Get a cash advance (GET /cashadvance/v4/cashadvances/\{cashadvanceId\}). +Get a cash advance (GET /cashadvance/v4.1/cashadvances/\{cashAdvanceId\}). #### Input @@ -988,7 +988,7 @@ Get expense itemizations (GET /expensereports/v4/users/\{userId\}/context/\{cont | `password` | string | No | Password \(only for password grant\) | | `companyUuid` | string | No | Company UUID for multi-company access tokens | | `userId` | string | Yes | Concur user UUID | -| `contextType` | string | Yes | Access context \(TRAVELER per the v4 spec\) | +| `contextType` | string | Yes | Access context: TRAVELER, MANAGER, or PROXY | | `reportId` | string | Yes | Expense report ID | | `expenseId` | string | Yes | Expense ID | @@ -1426,7 +1426,7 @@ Get a single user by UUID (GET /profile/identity/v4.1/Users/\{id\}). ### `sap_concur_issue_cash_advance` -Issue a cash advance (POST /cashadvance/v4/cashadvances/\{cashadvanceId\}/issue). +Issue a cash advance (POST /cashadvance/v4.1/cashadvances/\{cashAdvanceId\}/issue). #### Input @@ -1651,7 +1651,7 @@ List expenses on a report (GET /expensereports/v4/users/\{userId\}/context/\{con | `password` | string | No | Password \(only for password grant\) | | `companyUuid` | string | No | Company UUID for multi-company access tokens | | `userId` | string | Yes | Concur user UUID | -| `contextType` | string | Yes | Access context \(TRAVELER per the v4 spec\) | +| `contextType` | string | Yes | Access context: TRAVELER, MANAGER, or PROXY | | `reportId` | string | Yes | Expense report ID | #### Output @@ -2081,10 +2081,10 @@ List travel profile summaries (GET /api/travelprofile/v2.0/summary). LastModifie | `username` | string | No | Username \(only for password grant\) | | `password` | string | No | Password \(only for password grant\) | | `companyUuid` | string | No | Company UUID for multi-company access tokens | -| `lastModifiedDate` | string | Yes | Required ISO 8601 date \(YYYY-MM-DD or full timestamp\) | +| `lastModifiedDate` | string | Yes | Required UTC datetime in YYYY-MM-DDThh:mm:ss format | | `page` | number | No | 1-based page number | | `itemsPerPage` | number | No | Items per page \(max 200\) | -| `active` | string | No | Status filter \(sent as Status query param\): "Active" or "Inactive". Omit for all. | +| `active` | string | No | Active filter \(sent as Active query param\): "1" \(active\) or "0" \(inactive\). Omit for all. | | `travelConfigs` | string | No | Comma-separated travel configuration ids | #### Output @@ -2238,6 +2238,7 @@ List Concur user identities (GET /profile/identity/v4.1/Users). | `companyUuid` | string | No | Company UUID for multi-company access tokens | | `count` | number | No | Max number of users to return \(default 100, max 1000\) | | `cursor` | string | No | SCIM v4.1 pagination cursor returned by a prior call | +| `filter` | string | No | SCIM filter expression. Supported attributes: userName, employeeNumber, externalId. | | `attributes` | string | No | Comma-separated list of attributes to include in the response | | `excludedAttributes` | string | No | Comma-separated list of attributes to exclude from the response | @@ -2318,7 +2319,7 @@ Recall a submitted expense report (PATCH /expensereports/v4/users/\{userId\}/con ### `sap_concur_remove_all_attendees` -Remove all attendees from an expense (DELETE /expensereports/v4/users/\{userId\}/context/TRAVELER/reports/\{reportId\}/expenses/\{expenseId\}/attendees). +Remove all attendees from an expense (DELETE /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses/\{expenseId\}/attendees). #### Input diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 84492a6464e..9a1513dc6f8 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -11570,11 +11570,11 @@ }, { "name": "Associate Attendees", - "description": "Associate attendees with an expense (POST /expensereports/v4/users/{userId}/context/TRAVELER/reports/{reportId}/expenses/{expenseId}/attendees)." + "description": "Associate attendees with an expense (POST /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/attendees)." }, { "name": "Remove All Attendees", - "description": "Remove all attendees from an expense (DELETE /expensereports/v4/users/{userId}/context/TRAVELER/reports/{reportId}/expenses/{expenseId}/attendees)." + "description": "Remove all attendees from an expense (DELETE /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/attendees)." }, { "name": "List Report Comments", @@ -11666,15 +11666,15 @@ }, { "name": "Create Cash Advance", - "description": "Create a cash advance (POST /cashadvance/v4/cashadvances)." + "description": "Create a cash advance (POST /cashadvance/v4.1/cashadvances)." }, { "name": "Get Cash Advance", - "description": "Get a cash advance (GET /cashadvance/v4/cashadvances/{cashadvanceId})." + "description": "Get a cash advance (GET /cashadvance/v4.1/cashadvances/{cashAdvanceId})." }, { "name": "Issue Cash Advance", - "description": "Issue a cash advance (POST /cashadvance/v4/cashadvances/{cashadvanceId}/issue)." + "description": "Issue a cash advance (POST /cashadvance/v4.1/cashadvances/{cashAdvanceId}/issue)." }, { "name": "List Itineraries (Trips)", diff --git a/apps/sim/tools/sap_concur/create_cash_advance.ts b/apps/sim/tools/sap_concur/create_cash_advance.ts index 6d507c2ee00..7297eb214c2 100644 --- a/apps/sim/tools/sap_concur/create_cash_advance.ts +++ b/apps/sim/tools/sap_concur/create_cash_advance.ts @@ -9,7 +9,7 @@ import type { ToolConfig } from '@/tools/types' export const createCashAdvanceTool: ToolConfig = { id: 'sap_concur_create_cash_advance', name: 'SAP Concur Create Cash Advance', - description: 'Create a cash advance (POST /cashadvance/v4/cashadvances).', + description: 'Create a cash advance (POST /cashadvance/v4.1/cashadvances).', version: '1.0.0', params: { datacenter: { @@ -67,7 +67,7 @@ export const createCashAdvanceTool: ToolConfig ({ 'Content-Type': 'application/json' }), body: (params) => ({ ...baseProxyBody(params), - path: `/cashadvance/v4/cashadvances`, + path: `/cashadvance/v4.1/cashadvances`, method: 'POST', body: params.body, }), diff --git a/apps/sim/tools/sap_concur/get_cash_advance.ts b/apps/sim/tools/sap_concur/get_cash_advance.ts index 9ef40958596..ace1a6315f1 100644 --- a/apps/sim/tools/sap_concur/get_cash_advance.ts +++ b/apps/sim/tools/sap_concur/get_cash_advance.ts @@ -10,7 +10,7 @@ import type { ToolConfig } from '@/tools/types' export const getCashAdvanceTool: ToolConfig = { id: 'sap_concur_get_cash_advance', name: 'SAP Concur Get Cash Advance', - description: 'Get a cash advance (GET /cashadvance/v4/cashadvances/{cashadvanceId}).', + description: 'Get a cash advance (GET /cashadvance/v4.1/cashadvances/{cashAdvanceId}).', version: '1.0.0', params: { datacenter: { @@ -70,7 +70,7 @@ export const getCashAdvanceTool: ToolConfig = { id: 'sap_concur_issue_cash_advance', name: 'SAP Concur Issue Cash Advance', - description: 'Issue a cash advance (POST /cashadvance/v4/cashadvances/{cashadvanceId}/issue).', + description: 'Issue a cash advance (POST /cashadvance/v4.1/cashadvances/{cashAdvanceId}/issue).', version: '1.0.0', params: { datacenter: { @@ -76,7 +76,7 @@ export const issueCashAdvanceTool: ToolConfig visibility: 'user-or-llm', description: 'SCIM v4.1 pagination cursor returned by a prior call', }, + filter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'SCIM filter expression. Supported attributes: userName, employeeNumber, externalId.', + }, attributes: { type: 'string', required: false, @@ -92,6 +99,7 @@ export const listUsersTool: ToolConfig query: buildListQuery({ count: params.count, cursor: params.cursor, + filter: params.filter, attributes: params.attributes, excludedAttributes: params.excludedAttributes, }), diff --git a/apps/sim/tools/sap_concur/types.ts b/apps/sim/tools/sap_concur/types.ts index 5d667af0261..426e02ec30d 100644 --- a/apps/sim/tools/sap_concur/types.ts +++ b/apps/sim/tools/sap_concur/types.ts @@ -327,6 +327,7 @@ export interface GetItineraryParams extends SapConcurBaseParams { export interface ListUsersParams extends SapConcurBaseParams { count?: number cursor?: string + filter?: string attributes?: string excludedAttributes?: string } From 6cc326b48714b50fab1f3f00980b87630ab573e6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 19:23:58 -0700 Subject: [PATCH 10/13] fix(sap-concur): drop SCIM list_users filter param (not supported on v4.1 GET) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SCIM Identity v4.1 GET /Users does not accept a filter query parameter — filtering is only supported via POST /Users/.search (already exposed by sap_concur_search_users). Co-Authored-By: Claude Opus 4.7 --- apps/sim/blocks/blocks/sap_concur.ts | 24 ++----------------- .../tools/sap_concur/create_report_comment.ts | 2 +- .../tools/sap_concur/list_report_comments.ts | 2 +- .../list_travel_profiles_summary.ts | 8 ------- apps/sim/tools/sap_concur/list_users.ts | 8 ------- apps/sim/tools/sap_concur/types.ts | 6 ++--- 6 files changed, 6 insertions(+), 44 deletions(-) diff --git a/apps/sim/blocks/blocks/sap_concur.ts b/apps/sim/blocks/blocks/sap_concur.ts index f7e83b6bfb4..ac5ede1dcc9 100644 --- a/apps/sim/blocks/blocks/sap_concur.ts +++ b/apps/sim/blocks/blocks/sap_concur.ts @@ -44,8 +44,6 @@ const EXPENSE_READ_CONTEXT_TYPE_OPS = [ 'sap_concur_list_expenses', 'sap_concur_get_expense', 'sap_concur_get_itemizations', - 'sap_concur_create_report_comment', - 'sap_concur_list_report_comments', 'sap_concur_list_exceptions', ] @@ -60,6 +58,8 @@ const ATTENDEE_CONTEXT_TYPE_OPS = [ 'sap_concur_list_attendee_associations', 'sap_concur_associate_attendees', 'sap_concur_remove_all_attendees', + 'sap_concur_create_report_comment', + 'sap_concur_list_report_comments', ] const ALLOCATION_CONTEXT_TYPE_OPS = [ @@ -1104,18 +1104,6 @@ export const SapConcurBlock: BlockConfig = { placeholder: '200', condition: { field: 'operation', value: 'sap_concur_list_travel_profiles_summary' }, }, - { - id: 'travelProfileActive', - title: 'Active Filter', - type: 'dropdown', - options: [ - { label: 'Any', id: '' }, - { label: 'Active', id: '1' }, - { label: 'Inactive', id: '0' }, - ], - value: () => '', - condition: { field: 'operation', value: 'sap_concur_list_travel_profiles_summary' }, - }, { id: 'travelConfigs', title: 'Travel Config IDs', @@ -1721,10 +1709,6 @@ export const SapConcurBlock: BlockConfig = { lastModifiedDate: params.lastModifiedDate, page: params.travelProfilePage ? Number(params.travelProfilePage) : undefined, itemsPerPage: params.itemsPerPage ? Number(params.itemsPerPage) : undefined, - active: - params.travelProfileActive === '1' || params.travelProfileActive === '0' - ? params.travelProfileActive - : undefined, travelConfigs: params.travelConfigs || undefined, } case 'sap_concur_search_locations': @@ -1885,10 +1869,6 @@ export const SapConcurBlock: BlockConfig = { page: { type: 'number', description: 'Page number (lists/list_items)' }, travelProfilePage: { type: 'number', description: 'Profile summary page number' }, itemsPerPage: { type: 'number', description: 'Profile summary items per page' }, - travelProfileActive: { - type: 'string', - description: 'Active filter ("1" for active, "0" for inactive)', - }, travelConfigs: { type: 'string', description: 'Comma-separated travel config ids' }, searchText: { type: 'string', description: 'Locations v5 free-text search' }, locCode: { type: 'string', description: 'Locations v5 location code' }, diff --git a/apps/sim/tools/sap_concur/create_report_comment.ts b/apps/sim/tools/sap_concur/create_report_comment.ts index 2fbb3f10252..f03271695c0 100644 --- a/apps/sim/tools/sap_concur/create_report_comment.ts +++ b/apps/sim/tools/sap_concur/create_report_comment.ts @@ -69,7 +69,7 @@ export const createReportCommentTool: ToolConfig< type: 'string', required: true, visibility: 'user-or-llm', - description: 'Access context: TRAVELER, MANAGER, or PROXY', + description: 'Access context: TRAVELER or PROXY', }, reportId: { type: 'string', diff --git a/apps/sim/tools/sap_concur/list_report_comments.ts b/apps/sim/tools/sap_concur/list_report_comments.ts index f618f4b3840..f585eae336b 100644 --- a/apps/sim/tools/sap_concur/list_report_comments.ts +++ b/apps/sim/tools/sap_concur/list_report_comments.ts @@ -68,7 +68,7 @@ export const listReportCommentsTool: ToolConfig visibility: 'user-or-llm', description: 'SCIM v4.1 pagination cursor returned by a prior call', }, - filter: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: - 'SCIM filter expression. Supported attributes: userName, employeeNumber, externalId.', - }, attributes: { type: 'string', required: false, @@ -99,7 +92,6 @@ export const listUsersTool: ToolConfig query: buildListQuery({ count: params.count, cursor: params.cursor, - filter: params.filter, attributes: params.attributes, excludedAttributes: params.excludedAttributes, }), diff --git a/apps/sim/tools/sap_concur/types.ts b/apps/sim/tools/sap_concur/types.ts index 426e02ec30d..86980bef61a 100644 --- a/apps/sim/tools/sap_concur/types.ts +++ b/apps/sim/tools/sap_concur/types.ts @@ -178,14 +178,14 @@ export interface RemoveAllAttendeesParams extends SapConcurBaseParams { export interface ListReportCommentsParams extends SapConcurBaseParams { userId: string - contextType: 'TRAVELER' | 'MANAGER' | 'PROXY' + contextType: 'TRAVELER' | 'PROXY' reportId: string includeAllComments?: boolean } export interface CreateReportCommentParams extends SapConcurBaseParams { userId: string - contextType: 'TRAVELER' | 'MANAGER' | 'PROXY' + contextType: 'TRAVELER' | 'PROXY' reportId: string comment: string } @@ -327,7 +327,6 @@ export interface GetItineraryParams extends SapConcurBaseParams { export interface ListUsersParams extends SapConcurBaseParams { count?: number cursor?: string - filter?: string attributes?: string excludedAttributes?: string } @@ -366,7 +365,6 @@ export interface ListTravelProfilesSummaryParams extends SapConcurBaseParams { lastModifiedDate: string page?: number itemsPerPage?: number - active?: 'Active' | 'Inactive' travelConfigs?: string } From 9ef3a11d758d2f398acee544137ad978f6a1eb45 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 19:25:22 -0700 Subject: [PATCH 11/13] fix(sap-concur): final live-API alignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified against live SAP Concur docs (concur/developer.concur.com preview branch): - Revert Cash Advance paths to /cashadvance/v4/ (v4.1 endpoints do not exist; live spec is v4) - Travel Profile v2 summary has no Active/Status query param — drop the filter from tool, types, and block - Report Comments v4 contextType is TRAVELER or PROXY only (NOT MANAGER) — move create_report_comment + list_report_comments into the TRAVELER/PROXY context group - Trip v1.1 query keys: userid_type / userid_value / ItemsPerPage / Page (snake/Pascal per docs) — already correct, kept Co-Authored-By: Claude Opus 4.7 --- apps/docs/content/docs/en/tools/sap_concur.mdx | 6 +++--- apps/sim/app/(landing)/integrations/data/integrations.json | 6 +++--- apps/sim/tools/sap_concur/create_cash_advance.ts | 4 ++-- apps/sim/tools/sap_concur/get_cash_advance.ts | 4 ++-- apps/sim/tools/sap_concur/issue_cash_advance.ts | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/docs/content/docs/en/tools/sap_concur.mdx b/apps/docs/content/docs/en/tools/sap_concur.mdx index 9abd0891879..d8a819c4e61 100644 --- a/apps/docs/content/docs/en/tools/sap_concur.mdx +++ b/apps/docs/content/docs/en/tools/sap_concur.mdx @@ -96,7 +96,7 @@ Associate attendees with an expense (POST /expensereports/v4/users/\{userId\}/co ### `sap_concur_create_cash_advance` -Create a cash advance (POST /cashadvance/v4.1/cashadvances). +Create a cash advance (POST /cashadvance/v4/cashadvances). #### Input @@ -686,7 +686,7 @@ Get a budget item header by ID (GET /budget/v4/budgetItemHeader/\{id\}). ### `sap_concur_get_cash_advance` -Get a cash advance (GET /cashadvance/v4.1/cashadvances/\{cashAdvanceId\}). +Get a cash advance (GET /cashadvance/v4/cashadvances/\{cashAdvanceId\}). #### Input @@ -1426,7 +1426,7 @@ Get a single user by UUID (GET /profile/identity/v4.1/Users/\{id\}). ### `sap_concur_issue_cash_advance` -Issue a cash advance (POST /cashadvance/v4.1/cashadvances/\{cashAdvanceId\}/issue). +Issue a cash advance (POST /cashadvance/v4/cashadvances/\{cashAdvanceId\}/issue). #### Input diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 9a1513dc6f8..8f5fea2387d 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -11666,15 +11666,15 @@ }, { "name": "Create Cash Advance", - "description": "Create a cash advance (POST /cashadvance/v4.1/cashadvances)." + "description": "Create a cash advance (POST /cashadvance/v4/cashadvances)." }, { "name": "Get Cash Advance", - "description": "Get a cash advance (GET /cashadvance/v4.1/cashadvances/{cashAdvanceId})." + "description": "Get a cash advance (GET /cashadvance/v4/cashadvances/{cashAdvanceId})." }, { "name": "Issue Cash Advance", - "description": "Issue a cash advance (POST /cashadvance/v4.1/cashadvances/{cashAdvanceId}/issue)." + "description": "Issue a cash advance (POST /cashadvance/v4/cashadvances/{cashAdvanceId}/issue)." }, { "name": "List Itineraries (Trips)", diff --git a/apps/sim/tools/sap_concur/create_cash_advance.ts b/apps/sim/tools/sap_concur/create_cash_advance.ts index 7297eb214c2..6d507c2ee00 100644 --- a/apps/sim/tools/sap_concur/create_cash_advance.ts +++ b/apps/sim/tools/sap_concur/create_cash_advance.ts @@ -9,7 +9,7 @@ import type { ToolConfig } from '@/tools/types' export const createCashAdvanceTool: ToolConfig = { id: 'sap_concur_create_cash_advance', name: 'SAP Concur Create Cash Advance', - description: 'Create a cash advance (POST /cashadvance/v4.1/cashadvances).', + description: 'Create a cash advance (POST /cashadvance/v4/cashadvances).', version: '1.0.0', params: { datacenter: { @@ -67,7 +67,7 @@ export const createCashAdvanceTool: ToolConfig ({ 'Content-Type': 'application/json' }), body: (params) => ({ ...baseProxyBody(params), - path: `/cashadvance/v4.1/cashadvances`, + path: `/cashadvance/v4/cashadvances`, method: 'POST', body: params.body, }), diff --git a/apps/sim/tools/sap_concur/get_cash_advance.ts b/apps/sim/tools/sap_concur/get_cash_advance.ts index ace1a6315f1..3e6ea57add7 100644 --- a/apps/sim/tools/sap_concur/get_cash_advance.ts +++ b/apps/sim/tools/sap_concur/get_cash_advance.ts @@ -10,7 +10,7 @@ import type { ToolConfig } from '@/tools/types' export const getCashAdvanceTool: ToolConfig = { id: 'sap_concur_get_cash_advance', name: 'SAP Concur Get Cash Advance', - description: 'Get a cash advance (GET /cashadvance/v4.1/cashadvances/{cashAdvanceId}).', + description: 'Get a cash advance (GET /cashadvance/v4/cashadvances/{cashAdvanceId}).', version: '1.0.0', params: { datacenter: { @@ -70,7 +70,7 @@ export const getCashAdvanceTool: ToolConfig = { id: 'sap_concur_issue_cash_advance', name: 'SAP Concur Issue Cash Advance', - description: 'Issue a cash advance (POST /cashadvance/v4.1/cashadvances/{cashAdvanceId}/issue).', + description: 'Issue a cash advance (POST /cashadvance/v4/cashadvances/{cashAdvanceId}/issue).', version: '1.0.0', params: { datacenter: { @@ -76,7 +76,7 @@ export const issueCashAdvanceTool: ToolConfig Date: Wed, 6 May 2026 19:26:41 -0700 Subject: [PATCH 12/13] docs --- apps/docs/content/docs/en/tools/sap_concur.mdx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/docs/content/docs/en/tools/sap_concur.mdx b/apps/docs/content/docs/en/tools/sap_concur.mdx index d8a819c4e61..8ceb2beccc6 100644 --- a/apps/docs/content/docs/en/tools/sap_concur.mdx +++ b/apps/docs/content/docs/en/tools/sap_concur.mdx @@ -322,7 +322,7 @@ Create a comment on a report (POST /expensereports/v4/users/\{userId\}/context/\ | `password` | string | No | Password \(only for password grant\) | | `companyUuid` | string | No | Company UUID for multi-company access tokens | | `userId` | string | Yes | Concur user UUID | -| `contextType` | string | Yes | Access context: TRAVELER, MANAGER, or PROXY | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | | `reportId` | string | Yes | Expense report ID | | `comment` | string | Yes | Comment text to add | @@ -1965,7 +1965,7 @@ List comments on a report (GET /expensereports/v4/users/\{userId\}/context/\{con | `password` | string | No | Password \(only for password grant\) | | `companyUuid` | string | No | Company UUID for multi-company access tokens | | `userId` | string | Yes | Concur user UUID | -| `contextType` | string | Yes | Access context: TRAVELER, MANAGER, or PROXY | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | | `reportId` | string | Yes | Expense report ID | | `includeAllComments` | boolean | No | Include comments from all expenses in the report \(default false\) | @@ -2084,7 +2084,6 @@ List travel profile summaries (GET /api/travelprofile/v2.0/summary). LastModifie | `lastModifiedDate` | string | Yes | Required UTC datetime in YYYY-MM-DDThh:mm:ss format | | `page` | number | No | 1-based page number | | `itemsPerPage` | number | No | Items per page \(max 200\) | -| `active` | string | No | Active filter \(sent as Active query param\): "1" \(active\) or "0" \(inactive\). Omit for all. | | `travelConfigs` | string | No | Comma-separated travel configuration ids | #### Output @@ -2238,7 +2237,6 @@ List Concur user identities (GET /profile/identity/v4.1/Users). | `companyUuid` | string | No | Company UUID for multi-company access tokens | | `count` | number | No | Max number of users to return \(default 100, max 1000\) | | `cursor` | string | No | SCIM v4.1 pagination cursor returned by a prior call | -| `filter` | string | No | SCIM filter expression. Supported attributes: userName, employeeNumber, externalId. | | `attributes` | string | No | Comma-separated list of attributes to include in the response | | `excludedAttributes` | string | No | Comma-separated list of attributes to exclude from the response | From d97640c61491f50e6e65298ad8c310c0a23fb11b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 6 May 2026 19:31:31 -0700 Subject: [PATCH 13/13] fix(sap-concur): restore Cash Advance v4.1 paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-verified against live developer.concur.com docs at /api-reference/cash-advance/v4-1.cash-advance.html — only v4.1 endpoints are documented: - POST /cashadvance/v4.1/cashadvances - GET /cashadvance/v4.1/cashadvances/{cashAdvanceId} - POST /cashadvance/v4.1/cashadvances/{cashAdvanceId}/issue The /cashadvance/v4/ docs page returns 404. Reverts the prior local rollback in 9ef3a11d7. Co-Authored-By: Claude Opus 4.7 --- apps/docs/content/docs/en/tools/sap_concur.mdx | 6 +++--- apps/sim/app/(landing)/integrations/data/integrations.json | 6 +++--- apps/sim/tools/sap_concur/create_cash_advance.ts | 4 ++-- apps/sim/tools/sap_concur/get_cash_advance.ts | 4 ++-- apps/sim/tools/sap_concur/issue_cash_advance.ts | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/docs/content/docs/en/tools/sap_concur.mdx b/apps/docs/content/docs/en/tools/sap_concur.mdx index 8ceb2beccc6..19ce53cb7f5 100644 --- a/apps/docs/content/docs/en/tools/sap_concur.mdx +++ b/apps/docs/content/docs/en/tools/sap_concur.mdx @@ -96,7 +96,7 @@ Associate attendees with an expense (POST /expensereports/v4/users/\{userId\}/co ### `sap_concur_create_cash_advance` -Create a cash advance (POST /cashadvance/v4/cashadvances). +Create a cash advance (POST /cashadvance/v4.1/cashadvances). #### Input @@ -686,7 +686,7 @@ Get a budget item header by ID (GET /budget/v4/budgetItemHeader/\{id\}). ### `sap_concur_get_cash_advance` -Get a cash advance (GET /cashadvance/v4/cashadvances/\{cashAdvanceId\}). +Get a cash advance (GET /cashadvance/v4.1/cashadvances/\{cashAdvanceId\}). #### Input @@ -1426,7 +1426,7 @@ Get a single user by UUID (GET /profile/identity/v4.1/Users/\{id\}). ### `sap_concur_issue_cash_advance` -Issue a cash advance (POST /cashadvance/v4/cashadvances/\{cashAdvanceId\}/issue). +Issue a cash advance (POST /cashadvance/v4.1/cashadvances/\{cashAdvanceId\}/issue). #### Input diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 8f5fea2387d..9a1513dc6f8 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -11666,15 +11666,15 @@ }, { "name": "Create Cash Advance", - "description": "Create a cash advance (POST /cashadvance/v4/cashadvances)." + "description": "Create a cash advance (POST /cashadvance/v4.1/cashadvances)." }, { "name": "Get Cash Advance", - "description": "Get a cash advance (GET /cashadvance/v4/cashadvances/{cashAdvanceId})." + "description": "Get a cash advance (GET /cashadvance/v4.1/cashadvances/{cashAdvanceId})." }, { "name": "Issue Cash Advance", - "description": "Issue a cash advance (POST /cashadvance/v4/cashadvances/{cashAdvanceId}/issue)." + "description": "Issue a cash advance (POST /cashadvance/v4.1/cashadvances/{cashAdvanceId}/issue)." }, { "name": "List Itineraries (Trips)", diff --git a/apps/sim/tools/sap_concur/create_cash_advance.ts b/apps/sim/tools/sap_concur/create_cash_advance.ts index 6d507c2ee00..7297eb214c2 100644 --- a/apps/sim/tools/sap_concur/create_cash_advance.ts +++ b/apps/sim/tools/sap_concur/create_cash_advance.ts @@ -9,7 +9,7 @@ import type { ToolConfig } from '@/tools/types' export const createCashAdvanceTool: ToolConfig = { id: 'sap_concur_create_cash_advance', name: 'SAP Concur Create Cash Advance', - description: 'Create a cash advance (POST /cashadvance/v4/cashadvances).', + description: 'Create a cash advance (POST /cashadvance/v4.1/cashadvances).', version: '1.0.0', params: { datacenter: { @@ -67,7 +67,7 @@ export const createCashAdvanceTool: ToolConfig ({ 'Content-Type': 'application/json' }), body: (params) => ({ ...baseProxyBody(params), - path: `/cashadvance/v4/cashadvances`, + path: `/cashadvance/v4.1/cashadvances`, method: 'POST', body: params.body, }), diff --git a/apps/sim/tools/sap_concur/get_cash_advance.ts b/apps/sim/tools/sap_concur/get_cash_advance.ts index 3e6ea57add7..ace1a6315f1 100644 --- a/apps/sim/tools/sap_concur/get_cash_advance.ts +++ b/apps/sim/tools/sap_concur/get_cash_advance.ts @@ -10,7 +10,7 @@ import type { ToolConfig } from '@/tools/types' export const getCashAdvanceTool: ToolConfig = { id: 'sap_concur_get_cash_advance', name: 'SAP Concur Get Cash Advance', - description: 'Get a cash advance (GET /cashadvance/v4/cashadvances/{cashAdvanceId}).', + description: 'Get a cash advance (GET /cashadvance/v4.1/cashadvances/{cashAdvanceId}).', version: '1.0.0', params: { datacenter: { @@ -70,7 +70,7 @@ export const getCashAdvanceTool: ToolConfig = { id: 'sap_concur_issue_cash_advance', name: 'SAP Concur Issue Cash Advance', - description: 'Issue a cash advance (POST /cashadvance/v4/cashadvances/{cashAdvanceId}/issue).', + description: 'Issue a cash advance (POST /cashadvance/v4.1/cashadvances/{cashAdvanceId}/issue).', version: '1.0.0', params: { datacenter: { @@ -76,7 +76,7 @@ export const issueCashAdvanceTool: ToolConfig