mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(sap): add SAP Concur integration block and SAP S/4HANA validation fixes (#4483)
* feat(sap): add SAP Concur integration block and SAP S/4HANA validation fixes
* added
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* finished
* docs
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* fix(sap-concur): drop SCIM list_users filter param (not supported on v4.1 GET)
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 <noreply@anthropic.com>
* fix(sap-concur): final live-API alignment
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 <noreply@anthropic.com>
* docs
* fix(sap-concur): restore Cash Advance v4.1 paths
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 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
7953c56aca
commit
a251e45400
@@ -4141,6 +4141,25 @@ export function SapS4HanaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function SapConcurIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 43.1 43.1'>
|
||||
<path
|
||||
fill='#F0AB00'
|
||||
d='M20.5,28.2c-3.6,0-6.6-3-6.6-6.6s2.9-6.6,6.6-6.6c1.8,0,3.5,0.7,4.6,1.9l3.4-3.4c-2.1-2.1-4.9-3.3-8.1-3.3 C14.1,10.2,9,15.3,9,21.6S14.1,33,20.4,33c3.1,0,6-1.3,8.1-3.3l-3.4-3.4C23.9,27.4,22.3,28.2,20.5,28.2'
|
||||
/>
|
||||
<path
|
||||
fill='#F0AB00'
|
||||
d='M30.1,18.7c-1.6,0-2.9,1.3-2.9,2.9s1.3,2.9,2.9,2.9c1.6,0,2.9-1.3,2.9-2.9C33,20,31.7,18.7,30.1,18.7'
|
||||
/>
|
||||
<path
|
||||
fill='#F0AB00'
|
||||
d='M0,43.1h43.1V0H0V43.1z M4.8,38.2V4.8h33.4v15.5v2.4v15.5C38.2,38.2,4.8,38.2,4.8,38.2z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceNowIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 71.1 63.6'>
|
||||
|
||||
@@ -155,6 +155,7 @@ import {
|
||||
RootlyIcon,
|
||||
S3Icon,
|
||||
SalesforceIcon,
|
||||
SapConcurIcon,
|
||||
SapS4HanaIcon,
|
||||
SESIcon,
|
||||
SearchIcon,
|
||||
@@ -372,6 +373,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
rootly: RootlyIcon,
|
||||
s3: S3Icon,
|
||||
salesforce: SalesforceIcon,
|
||||
sap_concur: SapConcurIcon,
|
||||
sap_s4hana: SapS4HanaIcon,
|
||||
search: SearchIcon,
|
||||
secrets_manager: SecretsManagerIcon,
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
"rootly",
|
||||
"s3",
|
||||
"salesforce",
|
||||
"sap_concur",
|
||||
"sap_s4hana",
|
||||
"search",
|
||||
"secrets_manager",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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 |
|
||||
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ import {
|
||||
RootlyIcon,
|
||||
S3Icon,
|
||||
SalesforceIcon,
|
||||
SapConcurIcon,
|
||||
SapS4HanaIcon,
|
||||
SESIcon,
|
||||
SearchIcon,
|
||||
@@ -354,6 +355,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
rootly: RootlyIcon,
|
||||
s3: S3Icon,
|
||||
salesforce: SalesforceIcon,
|
||||
sap_concur: SapConcurIcon,
|
||||
sap_s4hana: SapS4HanaIcon,
|
||||
search: SearchIcon,
|
||||
secrets_manager: SecretsManagerIcon,
|
||||
|
||||
@@ -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 with Items and 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/reports/{reportId}/approve). Required body field: comment."
|
||||
},
|
||||
{
|
||||
"name": "Send Back Expense Report",
|
||||
"description": "Send back an expense report to the employee (PATCH /expensereports/v4/reports/{reportId}/sendBack). Required body field: comment."
|
||||
},
|
||||
{
|
||||
"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/{contextType}/reports/{reportId}/expenses/{expenseId}/attendees)."
|
||||
},
|
||||
{
|
||||
"name": "Remove All Attendees",
|
||||
"description": "Remove all attendees from an expense (DELETE /expensereports/v4/users/{userId}/context/{contextType}/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.1/cashadvances)."
|
||||
},
|
||||
{
|
||||
"name": "Get Cash Advance",
|
||||
"description": "Get a cash advance (GET /cashadvance/v4.1/cashadvances/{cashAdvanceId})."
|
||||
},
|
||||
{
|
||||
"name": "Issue Cash Advance",
|
||||
"description": "Issue a cash advance (POST /cashadvance/v4.1/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": "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",
|
||||
"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,
|
||||
|
||||
@@ -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<Invocation> {
|
||||
const url = assertSafeExternalUrl(buildApiUrl(geolocation, req), 'apiUrl').toString()
|
||||
const hasBody = req.body !== undefined && req.body !== null
|
||||
const headers: Record<string, string> = {
|
||||
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 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,305 @@
|
||||
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'])
|
||||
|
||||
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<typeof SapConcurAuthSchema>
|
||||
|
||||
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<typeof SapConcurProxyRequestSchema>
|
||||
|
||||
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<typeof SapConcurUploadRequestSchema>
|
||||
|
||||
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<string, CachedToken>()
|
||||
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)
|
||||
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, {
|
||||
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<string, unknown>
|
||||
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<string, unknown>
|
||||
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}`
|
||||
}
|
||||
@@ -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<string, string>
|
||||
): Promise<UploadInvocation> {
|
||||
const headers: Record<string, string> = {
|
||||
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<UploadInvocation> {
|
||||
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<string, string> | 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<UploadInvocation> {
|
||||
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 })
|
||||
}
|
||||
})
|
||||
@@ -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<s
|
||||
const tokenUrl = assertSafeSapExternalUrl(resolveTokenUrl(req), 'tokenUrl').toString()
|
||||
const basic = Buffer.from(`${req.clientId}:${req.clientSecret}`).toString('base64')
|
||||
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${basic}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json',
|
||||
const response = await secureFetchWithValidation(
|
||||
tokenUrl,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${basic}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: 'grant_type=client_credentials',
|
||||
timeout: OUTBOUND_FETCH_TIMEOUT_MS,
|
||||
},
|
||||
body: 'grant_type=client_credentials',
|
||||
signal: AbortSignal.timeout(OUTBOUND_FETCH_TIMEOUT_MS),
|
||||
})
|
||||
'tokenUrl'
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '')
|
||||
@@ -104,12 +112,9 @@ 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]+=)/)
|
||||
return cookies
|
||||
function joinSetCookies(response: SecureFetchResponse): string {
|
||||
return response.headers
|
||||
.getSetCookie()
|
||||
.map((c) => c.split(';')[0]?.trim())
|
||||
.filter(Boolean)
|
||||
.join('; ')
|
||||
@@ -129,15 +134,19 @@ async function fetchCsrf(
|
||||
requestId: string
|
||||
): Promise<CsrfBundle | null> {
|
||||
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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,25 @@ export const SapS4HanaBlock: BlockConfig<SapProxyResponse> = {
|
||||
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<SapProxyResponse> = {
|
||||
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<SapProxyResponse> = {
|
||||
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<SapProxyResponse> = {
|
||||
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',
|
||||
},
|
||||
|
||||
@@ -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<string, BlockConfig> = {
|
||||
rss: RssBlock,
|
||||
s3: S3Block,
|
||||
salesforce: SalesforceBlock,
|
||||
sap_concur: SapConcurBlock,
|
||||
sap_s4hana: SapS4HanaBlock,
|
||||
schedule: ScheduleBlock,
|
||||
search: SearchBlock,
|
||||
|
||||
@@ -4141,6 +4141,25 @@ export function SapS4HanaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function SapConcurIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 43.1 43.1'>
|
||||
<path
|
||||
fill='#F0AB00'
|
||||
d='M20.5,28.2c-3.6,0-6.6-3-6.6-6.6s2.9-6.6,6.6-6.6c1.8,0,3.5,0.7,4.6,1.9l3.4-3.4c-2.1-2.1-4.9-3.3-8.1-3.3 C14.1,10.2,9,15.3,9,21.6S14.1,33,20.4,33c3.1,0,6-1.3,8.1-3.3l-3.4-3.4C23.9,27.4,22.3,28.2,20.5,28.2'
|
||||
/>
|
||||
<path
|
||||
fill='#F0AB00'
|
||||
d='M30.1,18.7c-1.6,0-2.9,1.3-2.9,2.9s1.3,2.9,2.9,2.9c1.6,0,2.9-1.3,2.9-2.9C33,20,31.7,18.7,30.1,18.7'
|
||||
/>
|
||||
<path
|
||||
fill='#F0AB00'
|
||||
d='M0,43.1h43.1V0H0V43.1z M4.8,38.2V4.8h33.4v15.5v2.4v15.5C38.2,38.2,4.8,38.2,4.8,38.2z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceNowIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 71.1 63.6'>
|
||||
|
||||
@@ -217,15 +217,22 @@ export interface SecureFetchOptions {
|
||||
|
||||
export class SecureFetchHeaders {
|
||||
private headers: Map<string, string>
|
||||
private setCookies: string[]
|
||||
|
||||
constructor(headers: Record<string, string>) {
|
||||
constructor(headers: Record<string, string>, 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<string, string> {
|
||||
const record: Record<string, string> = {}
|
||||
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<string, string> = {}
|
||||
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 () =>
|
||||
|
||||
@@ -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,
|
||||
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,
|
||||
uploadExchangeRatesTool as sapConcurUploadExchangeRatesTool,
|
||||
uploadReceiptImageTool as sapConcurUploadReceiptImageTool,
|
||||
} from '@/tools/sap_concur'
|
||||
import {
|
||||
createBusinessPartnerTool as sapS4HanaCreateBusinessPartnerTool,
|
||||
createPurchaseOrderTool as sapS4HanaCreatePurchaseOrderTool,
|
||||
@@ -5370,6 +5442,76 @@ export const tools: Record<string, ToolConfig> = {
|
||||
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_upload_exchange_rates: sapConcurUploadExchangeRatesTool,
|
||||
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,
|
||||
|
||||
@@ -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) or password',
|
||||
},
|
||||
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)' },
|
||||
},
|
||||
}
|
||||
@@ -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<AssociateAttendeesParams, SapConcurProxyResponse> =
|
||||
{
|
||||
id: 'sap_concur_associate_attendees',
|
||||
name: 'SAP Concur Associate Attendees',
|
||||
description:
|
||||
'Associate attendees with an expense (POST /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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<CreateCashAdvanceParams, SapConcurProxyResponse> = {
|
||||
id: 'sap_concur_create_cash_advance',
|
||||
name: 'SAP Concur Create Cash Advance',
|
||||
description: 'Create a cash advance (POST /cashadvance/v4.1/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) or password',
|
||||
},
|
||||
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.1/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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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<string, string> = {}
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<CreateListItemParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<CreateQuickExpenseParams, SapConcurProxyResponse> =
|
||||
{
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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<string, string> = {}
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<CreateUserParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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<string, string> = {}
|
||||
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: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<DeleteExpenseParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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)' },
|
||||
},
|
||||
}
|
||||
@@ -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<DeleteListItemParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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<string, string> = {}
|
||||
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: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<DeleteUserParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetAllocationParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetBudgetParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetCashAdvanceParams, SapConcurProxyResponse> = {
|
||||
id: 'sap_concur_get_cash_advance',
|
||||
name: 'SAP Concur Get Cash Advance',
|
||||
description: 'Get a cash advance (GET /cashadvance/v4.1/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) or password',
|
||||
},
|
||||
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.1/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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetExpectedExpenseParams, SapConcurProxyResponse> =
|
||||
{
|
||||
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) or password',
|
||||
},
|
||||
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<string, string> = {}
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetExpenseParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetExpenseReportParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetItemizationsParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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)}/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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetItineraryParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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({
|
||||
userid_type: params.useridType,
|
||||
userid_value: 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetListParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetListItemParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetPurchaseRequestParams, SapConcurProxyResponse> =
|
||||
{
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetReceiptParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetReceiptStatusParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetTravelProfileParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetTravelRequestParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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<string, string> = {}
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<GetUserParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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<string, string> = {}
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 { 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 { uploadExchangeRatesTool } from '@/tools/sap_concur/upload_exchange_rates'
|
||||
export { uploadReceiptImageTool } from '@/tools/sap_concur/upload_receipt_image'
|
||||
@@ -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<IssueCashAdvanceParams, SapConcurProxyResponse> = {
|
||||
id: 'sap_concur_issue_cash_advance',
|
||||
name: 'SAP Concur Issue Cash Advance',
|
||||
description: 'Issue a cash advance (POST /cashadvance/v4.1/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) or password',
|
||||
},
|
||||
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.1/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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ListAllocationsParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ListBudgetsParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ListExceptionsParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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<string, string> = {}
|
||||
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}.',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ListExpenseReportsParams, SapConcurProxyResponse> =
|
||||
{
|
||||
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 with Items and 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) or password',
|
||||
},
|
||||
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<string, string | number> = {}
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ListExpensesParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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)}/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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ListItinerariesParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
userid_type: params.useridType,
|
||||
userid_value: 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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ListListItemsParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ListListsParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ListReceiptsParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ListReportCommentsParams, SapConcurProxyResponse> =
|
||||
{
|
||||
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) or password',
|
||||
},
|
||||
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',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
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) or password',
|
||||
},
|
||||
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 UTC datetime in YYYY-MM-DDThh:mm:ss format',
|
||||
},
|
||||
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)',
|
||||
},
|
||||
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,
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ListTravelRequestsParams, SapConcurProxyResponse> =
|
||||
{
|
||||
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) or password',
|
||||
},
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ListUsersParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<MoveTravelRequestParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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<string, string> = {}
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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)' },
|
||||
},
|
||||
}
|
||||
@@ -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<RemoveAllAttendeesParams, SapConcurProxyResponse> =
|
||||
{
|
||||
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/{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) or password',
|
||||
},
|
||||
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: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<SearchLocationsParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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<string, string | number> = {}
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<SearchUsersParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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)' },
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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)' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
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'
|
||||
|
||||
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, unknown> | string
|
||||
}
|
||||
|
||||
export interface UpdateExpenseReportParams extends SapConcurBaseParams {
|
||||
userId: string
|
||||
contextType: 'TRAVELER' | 'PROXY'
|
||||
reportId: string
|
||||
body: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface DeleteExpenseReportParams extends SapConcurBaseParams {
|
||||
reportId: string
|
||||
}
|
||||
|
||||
export interface SubmitExpenseReportParams extends SapConcurBaseParams {
|
||||
userId: string
|
||||
reportId: string
|
||||
body?: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface RecallExpenseReportParams extends SapConcurBaseParams {
|
||||
userId: string
|
||||
contextType: 'TRAVELER' | 'PROXY'
|
||||
reportId: string
|
||||
body?: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface ApproveExpenseReportParams extends SapConcurBaseParams {
|
||||
reportId: string
|
||||
body: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface SendBackExpenseReportParams extends SapConcurBaseParams {
|
||||
reportId: string
|
||||
body: Record<string, unknown> | 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, unknown> | 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, unknown> | 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, unknown> | string
|
||||
}
|
||||
|
||||
export interface RemoveAllAttendeesParams extends SapConcurBaseParams {
|
||||
userId: string
|
||||
contextType: 'TRAVELER' | 'PROXY'
|
||||
reportId: string
|
||||
expenseId: string
|
||||
}
|
||||
|
||||
export interface ListReportCommentsParams extends SapConcurBaseParams {
|
||||
userId: string
|
||||
contextType: 'TRAVELER' | 'PROXY'
|
||||
reportId: string
|
||||
includeAllComments?: boolean
|
||||
}
|
||||
|
||||
export interface CreateReportCommentParams extends SapConcurBaseParams {
|
||||
userId: string
|
||||
contextType: 'TRAVELER' | '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, unknown> | 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, unknown> | string
|
||||
}
|
||||
|
||||
export interface UpdateTravelRequestParams extends SapConcurBaseParams {
|
||||
requestUuid: string
|
||||
body: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface DeleteTravelRequestParams extends SapConcurBaseParams {
|
||||
requestUuid: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
export interface MoveTravelRequestParams extends SapConcurBaseParams {
|
||||
requestUuid: string
|
||||
action: string
|
||||
userId?: string
|
||||
body?: Record<string, unknown> | 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, unknown> | 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, unknown> | 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, unknown> | string
|
||||
}
|
||||
|
||||
export interface IssueCashAdvanceParams extends SapConcurBaseParams {
|
||||
cashAdvanceId: string
|
||||
body?: Record<string, unknown> | 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, unknown> | string
|
||||
}
|
||||
|
||||
export interface GetUserParams extends SapConcurBaseParams {
|
||||
userUuid: string
|
||||
attributes?: string
|
||||
excludedAttributes?: string
|
||||
}
|
||||
|
||||
export interface CreateUserParams extends SapConcurBaseParams {
|
||||
body: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface UpdateUserParams extends SapConcurBaseParams {
|
||||
userUuid: string
|
||||
body: Record<string, unknown> | 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
|
||||
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, unknown> | string
|
||||
}
|
||||
|
||||
export interface UpdateListItemParams extends SapConcurBaseParams {
|
||||
itemId: string
|
||||
body: Record<string, unknown> | 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, unknown> | 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, unknown> | string
|
||||
}
|
||||
|
||||
export interface UpdatePurchaseRequestParams extends SapConcurBaseParams {
|
||||
purchaseRequestId: string
|
||||
body: Record<string, unknown> | 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 UploadExchangeRatesParams extends SapConcurBaseParams {
|
||||
body: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface ListLocalitiesParams extends SapConcurBaseParams {
|
||||
limit?: number
|
||||
offset?: number
|
||||
countryCode?: string
|
||||
}
|
||||
@@ -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<UpdateAllocationParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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<string, string> = {}
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<UpdateExpenseParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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)' },
|
||||
},
|
||||
}
|
||||
@@ -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<UpdateListItemParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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) or password',
|
||||
},
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<UpdateUserParams, SapConcurProxyResponse> = {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { SapConcurProxyResponse, UploadExchangeRatesParams } 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 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 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: {
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<UploadReceiptImageParams, SapConcurProxyResponse> =
|
||||
{
|
||||
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) or password',
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
import type { SapConcurBaseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types'
|
||||
import type { OutputProperty } from '@/tools/types'
|
||||
|
||||
export const scimUserOutputProperties: Record<string, OutputProperty> = {
|
||||
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<string, OutputProperty> = {
|
||||
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<string, unknown> {
|
||||
const body: Record<string, unknown> = {
|
||||
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<string, string | number | boolean | undefined | null>
|
||||
): Record<string, string | number | boolean> {
|
||||
const query: Record<string, string | number | boolean> = {}
|
||||
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<SapConcurProxyResponse> {
|
||||
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()
|
||||
}
|
||||
@@ -17,26 +17,26 @@ export const createBusinessPartnerTool: ToolConfig<CreateBusinessPartnerParams,
|
||||
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',
|
||||
},
|
||||
@@ -156,7 +156,63 @@ export const createBusinessPartnerTool: ToolConfig<CreateBusinessPartnerParams,
|
||||
},
|
||||
transformResponse: transformSapProxyResponse,
|
||||
outputs: {
|
||||
status: { type: 'number', description: 'HTTP status code returned by SAP' },
|
||||
data: { type: 'json', description: 'Created A_BusinessPartner entity' },
|
||||
status: { type: 'number', description: 'HTTP status code returned by SAP (201 on success)' },
|
||||
data: {
|
||||
type: 'json',
|
||||
description: 'Created A_BusinessPartner entity (under d in OData v2)',
|
||||
properties: {
|
||||
BusinessPartner: {
|
||||
type: 'string',
|
||||
description: 'Generated business partner key (up to 10 chars)',
|
||||
},
|
||||
BusinessPartnerFullName: {
|
||||
type: 'string',
|
||||
description: 'Full name (concatenated first/last or organization name)',
|
||||
optional: true,
|
||||
},
|
||||
BusinessPartnerCategory: {
|
||||
type: 'string',
|
||||
description: '"1" Person, "2" Organization, "3" Group',
|
||||
optional: true,
|
||||
},
|
||||
BusinessPartnerGrouping: {
|
||||
type: 'string',
|
||||
description: 'Grouping / number range used to assign the key',
|
||||
optional: true,
|
||||
},
|
||||
BusinessPartnerType: {
|
||||
type: 'string',
|
||||
description: 'Business partner type (tenant-configured)',
|
||||
optional: true,
|
||||
},
|
||||
BusinessPartnerUUID: {
|
||||
type: 'string',
|
||||
description: 'GUID identifier for the business partner',
|
||||
optional: true,
|
||||
},
|
||||
FirstName: { type: 'string', description: 'First name (Person)', optional: true },
|
||||
LastName: { type: 'string', description: 'Last name (Person)', optional: true },
|
||||
OrganizationBPName1: {
|
||||
type: 'string',
|
||||
description: 'Organization name line 1',
|
||||
optional: true,
|
||||
},
|
||||
CreationDate: {
|
||||
type: 'string',
|
||||
description: 'Date the partner was created (OData /Date(...)/ literal)',
|
||||
optional: true,
|
||||
},
|
||||
CreatedByUser: {
|
||||
type: 'string',
|
||||
description: 'User who created the business partner',
|
||||
optional: true,
|
||||
},
|
||||
LastChangeDate: {
|
||||
type: 'string',
|
||||
description: 'Date of last change (OData /Date(...)/ literal)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,26 +16,26 @@ export const createPurchaseOrderTool: ToolConfig<CreatePurchaseOrderParams, SapP
|
||||
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',
|
||||
},
|
||||
@@ -146,6 +146,43 @@ export const createPurchaseOrderTool: ToolConfig<CreatePurchaseOrderParams, SapP
|
||||
transformResponse: transformSapProxyResponse,
|
||||
outputs: {
|
||||
status: { type: 'number', description: 'HTTP status code returned by SAP' },
|
||||
data: { type: 'json', description: 'Created A_PurchaseOrder entity' },
|
||||
data: {
|
||||
type: 'json',
|
||||
description: 'OData v2 response envelope; created entity at output.data.d',
|
||||
properties: {
|
||||
d: {
|
||||
type: 'json',
|
||||
description: 'Created A_PurchaseOrder entity',
|
||||
properties: {
|
||||
PurchaseOrder: { type: 'string', description: 'Auto-assigned purchase order number' },
|
||||
PurchaseOrderType: { type: 'string', description: 'PO document type' },
|
||||
CompanyCode: { type: 'string', description: 'Company code' },
|
||||
PurchasingOrganization: { type: 'string', description: 'Purchasing organization' },
|
||||
PurchasingGroup: { type: 'string', description: 'Purchasing group' },
|
||||
Supplier: { type: 'string', description: 'Supplier business partner key' },
|
||||
DocumentCurrency: {
|
||||
type: 'string',
|
||||
description: 'Document currency',
|
||||
optional: true,
|
||||
},
|
||||
NetAmount: {
|
||||
type: 'string',
|
||||
description: 'Net amount of the purchase order',
|
||||
optional: true,
|
||||
},
|
||||
CreationDate: {
|
||||
type: 'string',
|
||||
description: 'Creation date (OData /Date(ms)/)',
|
||||
optional: true,
|
||||
},
|
||||
to_PurchaseOrderItem: {
|
||||
type: 'json',
|
||||
description: 'Created PO items returned in deep insert',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -19,26 +19,26 @@ export const createPurchaseRequisitionTool: 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',
|
||||
},
|
||||
@@ -96,7 +96,7 @@ export const createPurchaseRequisitionTool: ToolConfig<
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Additional A_PurchaseRequisitionHeader fields merged into the create payload (e.g., {"PurchaseRequisitionDescription":"Office supplies"})',
|
||||
'Additional A_PurchaseRequisitionHeader fields merged into the create payload (e.g., {"PurReqnDescription":"Office supplies"})',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
@@ -127,6 +127,40 @@ export const createPurchaseRequisitionTool: ToolConfig<
|
||||
transformResponse: transformSapProxyResponse,
|
||||
outputs: {
|
||||
status: { type: 'number', description: 'HTTP status code returned by SAP' },
|
||||
data: { type: 'json', description: 'Created A_PurchaseRequisitionHeader entity' },
|
||||
data: {
|
||||
type: 'json',
|
||||
description: 'OData v2 response envelope; created entity at output.data.d',
|
||||
properties: {
|
||||
d: {
|
||||
type: 'json',
|
||||
description: 'Created A_PurchaseRequisitionHeader entity',
|
||||
properties: {
|
||||
PurchaseRequisition: {
|
||||
type: 'string',
|
||||
description: 'Auto-assigned purchase requisition number',
|
||||
},
|
||||
PurchaseRequisitionType: {
|
||||
type: 'string',
|
||||
description: 'PR document type (e.g., NB)',
|
||||
},
|
||||
PurReqnDescription: {
|
||||
type: 'string',
|
||||
description: 'Purchase requisition description',
|
||||
optional: true,
|
||||
},
|
||||
SourceDetermination: {
|
||||
type: 'string',
|
||||
description: 'Source-of-supply determination flag',
|
||||
optional: true,
|
||||
},
|
||||
to_PurchaseReqnItem: {
|
||||
type: 'json',
|
||||
description: 'Created PR items returned in deep insert',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,26 +16,26 @@ export const createSalesOrderTool: ToolConfig<CreateSalesOrderParams, SapProxyRe
|
||||
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',
|
||||
},
|
||||
@@ -150,10 +150,42 @@ export const createSalesOrderTool: ToolConfig<CreateSalesOrderParams, SapProxyRe
|
||||
},
|
||||
transformResponse: transformSapProxyResponse,
|
||||
outputs: {
|
||||
status: { type: 'number', description: 'HTTP status code returned by SAP' },
|
||||
status: { type: 'number', description: 'HTTP status code returned by SAP (201 on create)' },
|
||||
data: {
|
||||
type: 'json',
|
||||
description: 'Created A_SalesOrder entity (with deep-inserted items if expanded by SAP)',
|
||||
description: 'OData v2 response envelope; created entity at output.data.d',
|
||||
properties: {
|
||||
d: {
|
||||
type: 'json',
|
||||
description: 'Created A_SalesOrder entity',
|
||||
properties: {
|
||||
SalesOrder: { type: 'string', description: 'Newly assigned sales order number' },
|
||||
SalesOrderType: { type: 'string', description: 'Sales document type' },
|
||||
SalesOrganization: { type: 'string', description: 'Sales organization' },
|
||||
DistributionChannel: { type: 'string', description: 'Distribution channel' },
|
||||
OrganizationDivision: { type: 'string', description: 'Division' },
|
||||
SoldToParty: { type: 'string', description: 'Sold-to business partner' },
|
||||
TotalNetAmount: { type: 'string', description: 'Total net amount' },
|
||||
TransactionCurrency: { type: 'string', description: 'Document currency' },
|
||||
CreationDate: { type: 'string', description: 'Creation date' },
|
||||
OverallSDProcessStatus: {
|
||||
type: 'string',
|
||||
description: 'Overall sales document process status',
|
||||
optional: true,
|
||||
},
|
||||
OverallTotalDeliveryStatus: {
|
||||
type: 'string',
|
||||
description: 'Overall total delivery status',
|
||||
optional: true,
|
||||
},
|
||||
to_Item: {
|
||||
type: 'json',
|
||||
description: 'Deep-inserted sales order items as returned by SAP',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,26 +16,26 @@ export const deleteSalesOrderTool: ToolConfig<DeleteSalesOrderParams, SapProxyRe
|
||||
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',
|
||||
},
|
||||
@@ -103,6 +103,10 @@ export const deleteSalesOrderTool: ToolConfig<DeleteSalesOrderParams, SapProxyRe
|
||||
transformResponse: transformSapProxyResponse,
|
||||
outputs: {
|
||||
status: { type: 'number', description: 'HTTP status code returned by SAP (204 on success)' },
|
||||
data: { type: 'json', description: 'Null on successful deletion' },
|
||||
data: {
|
||||
type: 'json',
|
||||
description: 'Null on successful deletion (SAP returns 204 No Content)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,26 +17,26 @@ export const getBillingDocumentTool: ToolConfig<GetBillingDocumentParams, SapPro
|
||||
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',
|
||||
},
|
||||
@@ -92,7 +92,8 @@ export const getBillingDocumentTool: ToolConfig<GetBillingDocumentParams, SapPro
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated navigation properties to expand (e.g., "to_Item,to_Partner")',
|
||||
description:
|
||||
'Comma-separated navigation properties to expand (e.g., "to_Item,to_Partner,to_PricingElement")',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
@@ -110,6 +111,153 @@ export const getBillingDocumentTool: ToolConfig<GetBillingDocumentParams, SapPro
|
||||
transformResponse: transformSapProxyResponse,
|
||||
outputs: {
|
||||
status: { type: 'number', description: 'HTTP status code returned by SAP' },
|
||||
data: { type: 'json', description: 'A_BillingDocument entity' },
|
||||
data: {
|
||||
type: 'json',
|
||||
description: 'OData v2 response envelope; entity at output.data.d',
|
||||
properties: {
|
||||
d: {
|
||||
type: 'json',
|
||||
description: 'A_BillingDocument entity',
|
||||
properties: {
|
||||
BillingDocument: { type: 'string', description: 'Billing document number' },
|
||||
SDDocumentCategory: {
|
||||
type: 'string',
|
||||
description: 'SD document category',
|
||||
optional: true,
|
||||
},
|
||||
BillingDocumentCategory: {
|
||||
type: 'string',
|
||||
description: 'Billing document category',
|
||||
optional: true,
|
||||
},
|
||||
BillingDocumentType: {
|
||||
type: 'string',
|
||||
description: 'Billing document type',
|
||||
optional: true,
|
||||
},
|
||||
BillingDocumentDate: {
|
||||
type: 'string',
|
||||
description: 'Billing document date (OData /Date(ms)/)',
|
||||
optional: true,
|
||||
},
|
||||
BillingDocumentIsCancelled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the billing document is cancelled',
|
||||
optional: true,
|
||||
},
|
||||
CancelledBillingDocument: {
|
||||
type: 'string',
|
||||
description: 'Cancelled billing document number',
|
||||
optional: true,
|
||||
},
|
||||
TotalNetAmount: {
|
||||
type: 'string',
|
||||
description: 'Total net amount (Edm.Decimal as string)',
|
||||
optional: true,
|
||||
},
|
||||
TaxAmount: {
|
||||
type: 'string',
|
||||
description: 'Tax amount (Edm.Decimal as string)',
|
||||
optional: true,
|
||||
},
|
||||
TotalGrossAmount: {
|
||||
type: 'string',
|
||||
description: 'Total gross amount (Edm.Decimal as string)',
|
||||
optional: true,
|
||||
},
|
||||
TransactionCurrency: {
|
||||
type: 'string',
|
||||
description: 'Document currency',
|
||||
optional: true,
|
||||
},
|
||||
SoldToParty: {
|
||||
type: 'string',
|
||||
description: 'Sold-to business partner',
|
||||
optional: true,
|
||||
},
|
||||
PayerParty: { type: 'string', description: 'Payer party', optional: true },
|
||||
SalesOrganization: {
|
||||
type: 'string',
|
||||
description: 'Sales organization',
|
||||
optional: true,
|
||||
},
|
||||
DistributionChannel: {
|
||||
type: 'string',
|
||||
description: 'Distribution channel',
|
||||
optional: true,
|
||||
},
|
||||
Division: { type: 'string', description: 'Division', optional: true },
|
||||
CompanyCode: { type: 'string', description: 'Company code', optional: true },
|
||||
FiscalYear: { type: 'string', description: 'Fiscal year', optional: true },
|
||||
OverallBillingStatus: {
|
||||
type: 'string',
|
||||
description: 'Overall billing status',
|
||||
optional: true,
|
||||
},
|
||||
AccountingPostingStatus: {
|
||||
type: 'string',
|
||||
description: 'Accounting posting status',
|
||||
optional: true,
|
||||
},
|
||||
AccountingTransferStatus: {
|
||||
type: 'string',
|
||||
description: 'Accounting transfer status',
|
||||
optional: true,
|
||||
},
|
||||
InvoiceClearingStatus: {
|
||||
type: 'string',
|
||||
description: 'Invoice clearing status',
|
||||
optional: true,
|
||||
},
|
||||
AccountingDocument: {
|
||||
type: 'string',
|
||||
description: 'Linked accounting document',
|
||||
optional: true,
|
||||
},
|
||||
CustomerPaymentTerms: {
|
||||
type: 'string',
|
||||
description: 'Customer payment terms',
|
||||
optional: true,
|
||||
},
|
||||
PaymentMethod: { type: 'string', description: 'Payment method', optional: true },
|
||||
DocumentReferenceID: {
|
||||
type: 'string',
|
||||
description: 'Document reference ID',
|
||||
optional: true,
|
||||
},
|
||||
CreationDate: {
|
||||
type: 'string',
|
||||
description: 'Creation date (OData /Date(ms)/)',
|
||||
optional: true,
|
||||
},
|
||||
LastChangeDate: {
|
||||
type: 'string',
|
||||
description: 'Last change date (OData /Date(ms)/)',
|
||||
optional: true,
|
||||
},
|
||||
LastChangeDateTime: {
|
||||
type: 'string',
|
||||
description: 'Last change date-time (Edm.DateTimeOffset)',
|
||||
optional: true,
|
||||
},
|
||||
to_Item: {
|
||||
type: 'json',
|
||||
description: 'Billing document items (when $expand=to_Item)',
|
||||
optional: true,
|
||||
},
|
||||
to_Partner: {
|
||||
type: 'json',
|
||||
description: 'Billing document partners (when $expand=to_Partner)',
|
||||
optional: true,
|
||||
},
|
||||
to_PricingElement: {
|
||||
type: 'json',
|
||||
description: 'Billing document pricing elements (when $expand=to_PricingElement)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,26 +17,26 @@ export const getBusinessPartnerTool: ToolConfig<GetBusinessPartnerParams, SapPro
|
||||
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,76 @@ export const getBusinessPartnerTool: ToolConfig<GetBusinessPartnerParams, SapPro
|
||||
transformResponse: transformSapProxyResponse,
|
||||
outputs: {
|
||||
status: { type: 'number', description: 'HTTP status code returned by SAP' },
|
||||
data: { type: 'json', description: 'A_BusinessPartner entity' },
|
||||
data: {
|
||||
type: 'json',
|
||||
description: 'A_BusinessPartner entity (under d in OData v2)',
|
||||
properties: {
|
||||
BusinessPartner: { type: 'string', description: 'Business partner key (up to 10 chars)' },
|
||||
BusinessPartnerFullName: {
|
||||
type: 'string',
|
||||
description: 'Full name (concatenated first/last or organization name)',
|
||||
optional: true,
|
||||
},
|
||||
BusinessPartnerCategory: {
|
||||
type: 'string',
|
||||
description: '"1" Person, "2" Organization, "3" Group',
|
||||
optional: true,
|
||||
},
|
||||
BusinessPartnerGrouping: {
|
||||
type: 'string',
|
||||
description: 'Grouping / number range (tenant-configured)',
|
||||
optional: true,
|
||||
},
|
||||
BusinessPartnerType: {
|
||||
type: 'string',
|
||||
description: 'Business partner type (tenant-configured)',
|
||||
optional: true,
|
||||
},
|
||||
BusinessPartnerUUID: {
|
||||
type: 'string',
|
||||
description: 'GUID identifier for the business partner',
|
||||
optional: true,
|
||||
},
|
||||
BusinessPartnerIsBlocked: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the business partner is centrally blocked',
|
||||
optional: true,
|
||||
},
|
||||
FirstName: { type: 'string', description: 'First name (Person)', optional: true },
|
||||
LastName: { type: 'string', description: 'Last name (Person)', optional: true },
|
||||
OrganizationBPName1: {
|
||||
type: 'string',
|
||||
description: 'Organization name line 1',
|
||||
optional: true,
|
||||
},
|
||||
CorrespondenceLanguage: {
|
||||
type: 'string',
|
||||
description: 'Correspondence language (2-char code, e.g. "EN")',
|
||||
optional: true,
|
||||
},
|
||||
SearchTerm1: { type: 'string', description: 'Search term 1', optional: true },
|
||||
SearchTerm2: { type: 'string', description: 'Search term 2', optional: true },
|
||||
CreationDate: {
|
||||
type: 'string',
|
||||
description: 'Date the partner was created (OData /Date(...)/ literal)',
|
||||
optional: true,
|
||||
},
|
||||
CreatedByUser: {
|
||||
type: 'string',
|
||||
description: 'User who created the business partner',
|
||||
optional: true,
|
||||
},
|
||||
LastChangeDate: {
|
||||
type: 'string',
|
||||
description: 'Date of last change (OData /Date(...)/ literal)',
|
||||
optional: true,
|
||||
},
|
||||
LastChangedByUser: {
|
||||
type: 'string',
|
||||
description: 'User who last changed the business partner',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,26 +17,26 @@ export const getCustomerTool: ToolConfig<GetCustomerParams, SapProxyResponse> =
|
||||
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<GetCustomerParams, SapProxyResponse> =
|
||||
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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,26 +17,26 @@ export const getInboundDeliveryTool: ToolConfig<GetInboundDeliveryParams, SapPro
|
||||
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,85 @@ export const getInboundDeliveryTool: ToolConfig<GetInboundDeliveryParams, SapPro
|
||||
transformResponse: transformSapProxyResponse,
|
||||
outputs: {
|
||||
status: { type: 'number', description: 'HTTP status code returned by SAP' },
|
||||
data: { type: 'json', description: 'A_InbDeliveryHeader entity' },
|
||||
data: {
|
||||
type: 'json',
|
||||
description: 'OData v2 response envelope; entity at output.data.d',
|
||||
properties: {
|
||||
d: {
|
||||
type: 'json',
|
||||
description: 'A_InbDeliveryHeader entity',
|
||||
properties: {
|
||||
DeliveryDocument: { type: 'string', description: 'Inbound delivery number' },
|
||||
DeliveryDocumentType: { type: 'string', description: 'Delivery document type' },
|
||||
SDDocumentCategory: {
|
||||
type: 'string',
|
||||
description: 'SD document category (e.g., 7 = inbound delivery)',
|
||||
optional: true,
|
||||
},
|
||||
ReceivingPlant: { type: 'string', description: 'Receiving plant', optional: true },
|
||||
Supplier: {
|
||||
type: 'string',
|
||||
description: 'Supplier business partner',
|
||||
optional: true,
|
||||
},
|
||||
ShipToParty: {
|
||||
type: 'string',
|
||||
description: 'Ship-to business partner',
|
||||
optional: true,
|
||||
},
|
||||
DeliveryDate: {
|
||||
type: 'string',
|
||||
description: 'Delivery date (Edm.DateTime)',
|
||||
optional: true,
|
||||
},
|
||||
ActualGoodsMovementDate: {
|
||||
type: 'string',
|
||||
description: 'Actual goods movement (receipt) date (Edm.DateTime)',
|
||||
optional: true,
|
||||
},
|
||||
PlannedGoodsMovementDate: {
|
||||
type: 'string',
|
||||
description: 'Planned goods movement date (Edm.DateTime)',
|
||||
optional: true,
|
||||
},
|
||||
OverallSDProcessStatus: {
|
||||
type: 'string',
|
||||
description: 'Overall SD process (delivery) status',
|
||||
optional: true,
|
||||
},
|
||||
OverallGoodsMovementStatus: {
|
||||
type: 'string',
|
||||
description: 'Overall goods movement status',
|
||||
optional: true,
|
||||
},
|
||||
DocumentDate: {
|
||||
type: 'string',
|
||||
description: 'Document date (Edm.DateTime)',
|
||||
optional: true,
|
||||
},
|
||||
CreationDate: {
|
||||
type: 'string',
|
||||
description: 'Creation date (Edm.DateTime)',
|
||||
optional: true,
|
||||
},
|
||||
LastChangeDate: {
|
||||
type: 'string',
|
||||
description: 'Last change date (Edm.DateTime)',
|
||||
optional: true,
|
||||
},
|
||||
to_DeliveryDocumentItem: {
|
||||
type: 'json',
|
||||
description: 'Delivery items (when $expand=to_DeliveryDocumentItem)',
|
||||
optional: true,
|
||||
},
|
||||
to_DeliveryDocumentPartner: {
|
||||
type: 'json',
|
||||
description: 'Delivery partners (when $expand=to_DeliveryDocumentPartner)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user