From 7b6aa728aed4d7f5b79ecfffcfe9ae458e1feae7 Mon Sep 17 00:00:00 2001 From: Octopus Date: Wed, 6 May 2026 15:25:41 +0800 Subject: [PATCH 01/17] improvement(resolver): use context variables for block outputs in function block code (#4223) * v0.6.29: login improvements, posthog telemetry (#4026) * feat(posthog): Add tracking on mothership abort (#4023) Co-authored-by: Theodore Li * fix(login): fix captcha headers for manual login (#4025) * fix(signup): fix turnstile key loading * fix(login): fix captcha header passing * Catch user already exists, remove login form captcha * fix: use context variables for block outputs in function block code When a function block references another block's output via , the executor previously embedded the full value as a JavaScript literal directly in the code string. For large outputs (>50 KB), this caused the code string to exceed the terminal console display limit, making inputs appear truncated or replaced with { __simTruncated: true } in the UI. Instead, block output references in function block code are now stored as named global variables (__blockRef_N) in the isolated VM context. The code string only contains the compact variable name, keeping it small regardless of the referenced value size. Loop/parallel/env/workflow references are still inlined as literals since the API route has no way to resolve them independently. The _runtimeContextVars key is filtered from sanitizeInputsForLog so it does not appear in execution logs or SSE events. Pre-resolved context variables are merged with any variables produced by the API route resolveCodeVariables, with executor values taking precedence. Fixes #4195 * fix: address Cursor and Greptile bot review comments - Pass preResolvedContextVariables through to shellEnvs for Shell language (Cursor: shell loses pre-resolved block refs, executes against undefined vars) - Remove duplicate CodeExecutionOutput interface declaration (Cursor + Greptile: dead duplicate declaration in tools/function/types.ts) - Deduplicate identical block references in resolveCodeWithContextVars so the same reused multiple times shares one __blockRef_N slot (Greptile P2: avoid duplicating large payloads across the wire) * fix: shell block references and complex env value serialization Two follow-ups to the function-block context-variable refactor: - resolveCodeWithContextVars now emits `$__blockRef_N` for shell function blocks so the script dereferences the env var injected by the executor. Other languages still receive the bare identifier. - The function-execute route now JSON-stringifies non-primitive values when building shell env vars, replacing the previous `String(v)` call that produced `[object Object]` for objects/arrays. Co-Authored-By: Octopus * fix lint * review pass * ignore shell comments * update contract * fix tests --------- Co-authored-by: Waleed Co-authored-by: Theodore Li Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Co-authored-by: Vikhyath Mondreti Co-authored-by: octo-patch Co-authored-by: Vikhyath Mondreti --- apps/sim/app/api/function/execute/route.ts | 38 ++- apps/sim/executor/execution/block-executor.ts | 19 +- .../function/function-handler.test.ts | 28 +++ .../handlers/function/function-handler.ts | 5 + .../executor/utils/reference-validation.ts | 6 +- apps/sim/executor/variables/resolver.test.ts | 153 ++++++++++++ apps/sim/executor/variables/resolver.ts | 230 ++++++++++++++++++ apps/sim/lib/api/contracts/hotspots.ts | 1 + apps/sim/tools/function/execute.test.ts | 21 ++ apps/sim/tools/function/execute.ts | 1 + apps/sim/tools/function/types.ts | 2 + bun.lock | 1 - 12 files changed, 494 insertions(+), 11 deletions(-) create mode 100644 apps/sim/executor/variables/resolver.test.ts diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 89109b482ef..bd18d5686cd 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -587,6 +587,26 @@ function cleanStdout(stdout: string): string { return stdout } +/** + * Serializes a value for use as a shell environment variable. Strings pass through + * unchanged; primitives are coerced via `String`; objects, arrays, and other complex + * values are JSON-stringified so that referencing them via `$VAR` yields a useful + * representation instead of `[object Object]`. `null`/`undefined` become an empty + * string to match POSIX env semantics. + */ +function serializeForShellEnv(value: unknown, nullValue = ''): string { + if (value === null || value === undefined) return nullValue + if (typeof value === 'string') return value + if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') { + return String(value) + } + try { + return JSON.stringify(value) ?? '' + } catch { + return String(value) + } +} + async function maybeExportSandboxFileToWorkspace(args: { authUserId: string workflowId?: string @@ -722,6 +742,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { blockNameMapping = {}, blockOutputSchemas = {}, workflowVariables = {}, + contextVariables: preResolvedContextVariables = {}, workflowId, workspaceId, isCustomTool = false, @@ -746,6 +767,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => { // For shell, env vars are injected as OS env vars via shellEnvs. // Replace {{VAR}} placeholders with $VAR so the shell can access them natively. resolvedCode = code.replace(/\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}/g, '$$$1') + // Carry pre-resolved block output variables (e.g. __blockRef_N) so they can be + // injected as shell env vars below. The executor replaces block references in the + // code with these names, so the values must be present at runtime. + contextVariables = { ...preResolvedContextVariables } } else { const codeResolution = resolveCodeVariables( code, @@ -758,7 +783,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => { lang ) resolvedCode = codeResolution.resolvedCode - contextVariables = codeResolution.contextVariables + // Merge pre-resolved block output variables from the executor. These take precedence + // because they were produced by the resolver using full execution-state context + // (including loop/parallel scope) and should not be overwritten. + contextVariables = { ...codeResolution.contextVariables, ...preResolvedContextVariables } } let jsImports = '' @@ -783,10 +811,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => { const shellEnvs: Record = {} for (const [k, v] of Object.entries(envVars)) { - shellEnvs[k] = String(v) + shellEnvs[k] = serializeForShellEnv(v) } for (const [k, v] of Object.entries(contextVariables)) { - shellEnvs[k] = String(v) + shellEnvs[k] = serializeForShellEnv(v, 'null') } logger.info(`[${requestId}] E2B shell execution`, { @@ -893,7 +921,9 @@ export const POST = withRouteHandler(async (req: NextRequest) => { prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n` prologueLineCount++ for (const [k, v] of Object.entries(contextVariables)) { - prologue += `const ${k} = ${formatLiteralForCode(v, 'javascript')};\n` + prologue += `globalThis[${JSON.stringify(k)}] = ${formatLiteralForCode(v, 'javascript')};\n` + prologue += `const ${k} = globalThis[${JSON.stringify(k)}];\n` + prologueLineCount++ prologueLineCount++ } diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 9a3c22e8529..73cdcb8d674 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -44,7 +44,10 @@ import { } from '@/executor/utils/iteration-context' import { isJSONString } from '@/executor/utils/json' import { filterOutputForLog } from '@/executor/utils/output-filter' -import type { VariableResolver } from '@/executor/variables/resolver' +import { + FUNCTION_BLOCK_CONTEXT_VARS_KEY, + type VariableResolver, +} from '@/executor/variables/resolver' import type { SerializedBlock } from '@/serializer/types' import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants' @@ -115,7 +118,13 @@ export class BlockExecutor { await validateBlockType(ctx.userId, ctx.workspaceId, blockType, ctx) } - resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block) + if (block.metadata?.id === BlockType.FUNCTION) { + const { resolvedInputs: fnInputs, contextVariables } = + this.resolver.resolveInputsForFunctionBlock(ctx, node.id, block.config.params, block) + resolvedInputs = { ...fnInputs, [FUNCTION_BLOCK_CONTEXT_VARS_KEY]: contextVariables } + } else { + resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block) + } if (blockLog) { blockLog.input = this.sanitizeInputsForLog(resolvedInputs) @@ -428,7 +437,11 @@ export class BlockExecutor { const result: Record = {} for (const [key, value] of Object.entries(inputs)) { - if (SYSTEM_SUBBLOCK_IDS.includes(key) || key === 'triggerMode') { + if ( + SYSTEM_SUBBLOCK_IDS.includes(key) || + key === 'triggerMode' || + key === FUNCTION_BLOCK_CONTEXT_VARS_KEY + ) { continue } diff --git a/apps/sim/executor/handlers/function/function-handler.test.ts b/apps/sim/executor/handlers/function/function-handler.test.ts index 0dcda1ce37d..384540cc7b2 100644 --- a/apps/sim/executor/handlers/function/function-handler.test.ts +++ b/apps/sim/executor/handlers/function/function-handler.test.ts @@ -3,6 +3,7 @@ import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants' import { BlockType } from '@/executor/constants' import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler' import type { ExecutionContext } from '@/executor/types' +import { FUNCTION_BLOCK_CONTEXT_VARS_KEY } from '@/executor/variables/resolver' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' @@ -73,10 +74,13 @@ describe('FunctionBlockHandler', () => { blockData: {}, blockNameMapping: {}, blockOutputSchemas: {}, + contextVariables: {}, _context: { workflowId: mockContext.workflowId, workspaceId: mockContext.workspaceId, + userId: mockContext.userId, isDeployedContext: mockContext.isDeployedContext, + enforceCredentialAccess: mockContext.enforceCredentialAccess, }, } const expectedOutput: any = { result: 'Success' } @@ -110,10 +114,13 @@ describe('FunctionBlockHandler', () => { blockData: {}, blockNameMapping: {}, blockOutputSchemas: {}, + contextVariables: {}, _context: { workflowId: mockContext.workflowId, workspaceId: mockContext.workspaceId, + userId: mockContext.userId, isDeployedContext: mockContext.isDeployedContext, + enforceCredentialAccess: mockContext.enforceCredentialAccess, }, } const expectedOutput: any = { result: 'Success' } @@ -140,10 +147,13 @@ describe('FunctionBlockHandler', () => { blockData: {}, blockNameMapping: {}, blockOutputSchemas: {}, + contextVariables: {}, _context: { workflowId: mockContext.workflowId, workspaceId: mockContext.workspaceId, + userId: mockContext.userId, isDeployedContext: mockContext.isDeployedContext, + enforceCredentialAccess: mockContext.enforceCredentialAccess, }, } @@ -168,6 +178,24 @@ describe('FunctionBlockHandler', () => { expect(mockExecuteTool).toHaveBeenCalled() }) + it('should pass runtime context variables to function_execute', async () => { + const contextVariables = { __blockRef_0: { result: 'from-block' } } + + await handler.execute(mockContext, mockBlock, { + code: 'return globalThis["__blockRef_0"]', + [FUNCTION_BLOCK_CONTEXT_VARS_KEY]: contextVariables, + }) + + expect(mockExecuteTool).toHaveBeenCalledWith( + 'function_execute', + expect.objectContaining({ + contextVariables, + }), + false, + mockContext + ) + }) + it('should handle tool error with no specific message', async () => { const inputs = { code: 'some code' } const errorResult = { success: false } diff --git a/apps/sim/executor/handlers/function/function-handler.ts b/apps/sim/executor/handlers/function/function-handler.ts index 68302412bcb..c008a8d07ea 100644 --- a/apps/sim/executor/handlers/function/function-handler.ts +++ b/apps/sim/executor/handlers/function/function-handler.ts @@ -3,6 +3,7 @@ import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages' import { BlockType } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import { collectBlockData } from '@/executor/utils/block-data' +import { FUNCTION_BLOCK_CONTEXT_VARS_KEY } from '@/executor/variables/resolver' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' @@ -25,6 +26,9 @@ export class FunctionBlockHandler implements BlockHandler { const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx) + const contextVariables = + (inputs[FUNCTION_BLOCK_CONTEXT_VARS_KEY] as Record | undefined) ?? {} + const result = await executeTool( 'function_execute', { @@ -36,6 +40,7 @@ export class FunctionBlockHandler implements BlockHandler { blockData, blockNameMapping, blockOutputSchemas, + contextVariables, _context: { workflowId: ctx.workflowId, workspaceId: ctx.workspaceId, diff --git a/apps/sim/executor/utils/reference-validation.ts b/apps/sim/executor/utils/reference-validation.ts index 60e57b69639..18b2b76d89f 100644 --- a/apps/sim/executor/utils/reference-validation.ts +++ b/apps/sim/executor/utils/reference-validation.ts @@ -143,14 +143,14 @@ export function createCombinedPattern(): RegExp { */ export function replaceValidReferences( template: string, - replacer: (match: string) => string + replacer: (match: string, index: number, template: string) => string ): string { const pattern = createReferencePattern() - return template.replace(pattern, (match) => { + return template.replace(pattern, (match, _content, index) => { if (!isLikelyReferenceSegment(match)) { return match } - return replacer(match) + return replacer(match, index, template) }) } diff --git a/apps/sim/executor/variables/resolver.test.ts b/apps/sim/executor/variables/resolver.test.ts new file mode 100644 index 00000000000..915afa54ae6 --- /dev/null +++ b/apps/sim/executor/variables/resolver.test.ts @@ -0,0 +1,153 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { BlockType } from '@/executor/constants' +import { ExecutionState } from '@/executor/execution/state' +import type { ExecutionContext } from '@/executor/types' +import { VariableResolver } from '@/executor/variables/resolver' +import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' + +function createBlock(id: string, name: string, type: string, params = {}): SerializedBlock { + return { + id, + metadata: { id: type, name }, + position: { x: 0, y: 0 }, + config: { tool: type, params }, + inputs: {}, + outputs: { + result: 'string', + items: 'json', + }, + enabled: true, + } +} + +function createResolver(language = 'javascript') { + const producer = createBlock('producer', 'Producer', BlockType.API) + const functionBlock = createBlock('function', 'Function', BlockType.FUNCTION, { + language, + }) + const workflow: SerializedWorkflow = { + version: '1', + blocks: [producer, functionBlock], + connections: [], + loops: {}, + parallels: {}, + } + const state = new ExecutionState() + state.setBlockOutput('producer', { + result: 'hello world', + items: ['a', 'b'], + }) + const ctx = { + blockStates: state.getBlockStates(), + blockLogs: [], + environmentVariables: {}, + workflowVariables: {}, + decisions: { router: new Map(), condition: new Map() }, + loopExecutions: new Map(), + executedBlocks: new Set(), + activeExecutionPath: new Set(), + completedLoops: new Set(), + metadata: {}, + } as ExecutionContext + + return { + block: functionBlock, + ctx, + resolver: new VariableResolver(workflow, {}, state), + } +} + +describe('VariableResolver function block inputs', () => { + it('returns empty inputs when params are missing', () => { + const { block, ctx, resolver } = createResolver() + + const result = resolver.resolveInputsForFunctionBlock(ctx, 'function', undefined, block) + + expect(result).toEqual({ resolvedInputs: {}, contextVariables: {} }) + }) + + it('resolves JavaScript block references through globalThis context variables', () => { + const { block, ctx, resolver } = createResolver('javascript') + + const result = resolver.resolveInputsForFunctionBlock( + ctx, + 'function', + { code: 'return ' }, + block + ) + + expect(result.resolvedInputs.code).toBe('return globalThis["__blockRef_0"]') + expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' }) + }) + + it('resolves Python block references through globals lookup', () => { + const { block, ctx, resolver } = createResolver('python') + + const result = resolver.resolveInputsForFunctionBlock( + ctx, + 'function', + { code: 'return ' }, + block + ) + + expect(result.resolvedInputs.code).toBe('return globals()["__blockRef_0"]') + expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' }) + }) + + it('uses separate Python context variables for repeated mutable references', () => { + const { block, ctx, resolver } = createResolver('python') + + const result = resolver.resolveInputsForFunctionBlock( + ctx, + 'function', + { code: 'a = \nb = \nreturn b' }, + block + ) + + expect(result.resolvedInputs.code).toBe( + 'a = globals()["__blockRef_0"]\nb = globals()["__blockRef_1"]\nreturn b' + ) + expect(result.contextVariables).toEqual({ + __blockRef_0: ['a', 'b'], + __blockRef_1: ['a', 'b'], + }) + }) + + it('uses shell-safe expansions for block references', () => { + const { block, ctx, resolver } = createResolver('shell') + + const result = resolver.resolveInputsForFunctionBlock( + ctx, + 'function', + { code: 'echo suffix && echo ""' }, + block + ) + + expect(result.resolvedInputs.code).toBe( + `echo "\${__blockRef_0}"suffix && echo "\${__blockRef_1}"` + ) + expect(result.contextVariables).toEqual({ + __blockRef_0: 'hello world', + __blockRef_1: 'hello world', + }) + }) + + it('ignores shell comment quotes when formatting later block references', () => { + const { block, ctx, resolver } = createResolver('shell') + + const result = resolver.resolveInputsForFunctionBlock( + ctx, + 'function', + { code: "# don't confuse quote tracking\necho " }, + block + ) + + expect(result.resolvedInputs.code).toBe( + `# don't confuse quote tracking\necho "\${__blockRef_0}"` + ) + expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' }) + }) +}) diff --git a/apps/sim/executor/variables/resolver.ts b/apps/sim/executor/variables/resolver.ts index 412dd549d39..2cc9fd89e5b 100644 --- a/apps/sim/executor/variables/resolver.ts +++ b/apps/sim/executor/variables/resolver.ts @@ -16,8 +16,13 @@ import { import { WorkflowResolver } from '@/executor/variables/resolvers/workflow' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' +/** Key used to carry pre-resolved context variables through the inputs map. */ +export const FUNCTION_BLOCK_CONTEXT_VARS_KEY = '_runtimeContextVars' + const logger = createLogger('VariableResolver') +type ShellQuoteContext = 'single' | 'double' | null + export class VariableResolver { private resolvers: Resolver[] private blockResolver: BlockResolver @@ -37,6 +42,67 @@ export class VariableResolver { ] } + /** + * Resolves inputs for function blocks. Block output references in the `code` field + * are stored as named context variables instead of being embedded as JavaScript + * literals, preventing large values from bloating the code string. + * + * Returns the resolved inputs and a `contextVariables` map. Callers should inject + * contextVariables into the function execution request body so the isolated VM can + * access them as global variables. + */ + resolveInputsForFunctionBlock( + ctx: ExecutionContext, + currentNodeId: string, + params: Record | null | undefined, + block: SerializedBlock + ): { resolvedInputs: Record; contextVariables: Record } { + const contextVariables: Record = {} + const resolved: Record = {} + + if (!params) { + return { resolvedInputs: resolved, contextVariables } + } + + for (const [key, value] of Object.entries(params)) { + if (key === 'code') { + if (typeof value === 'string') { + resolved[key] = this.resolveCodeWithContextVars( + ctx, + currentNodeId, + value, + undefined, + block, + contextVariables + ) + } else if (Array.isArray(value)) { + resolved[key] = value.map((item: any) => { + if (item && typeof item === 'object' && typeof item.content === 'string') { + return { + ...item, + content: this.resolveCodeWithContextVars( + ctx, + currentNodeId, + item.content, + undefined, + block, + contextVariables + ), + } + } + return item + }) + } else { + resolved[key] = this.resolveValue(ctx, currentNodeId, value, undefined, block) + } + } else { + resolved[key] = this.resolveValue(ctx, currentNodeId, value, undefined, block) + } + } + + return { resolvedInputs: resolved, contextVariables } + } + resolveInputs( ctx: ExecutionContext, currentNodeId: string, @@ -150,6 +216,170 @@ export class VariableResolver { } return value } + /** + * Resolves a code template for a function block. Block output references are stored + * in `contextVarAccumulator` as named variables (e.g. `__blockRef_0`) and replaced + * with those variable names in the returned code string. Non-block references (loop + * items, workflow variables, env vars) are still inlined as literals so they remain + * available without any extra passing mechanism. + */ + private resolveCodeWithContextVars( + ctx: ExecutionContext, + currentNodeId: string, + template: string, + loopScope: LoopScope | undefined, + block: SerializedBlock, + contextVarAccumulator: Record + ): string { + const resolutionContext: ResolutionContext = { + executionContext: ctx, + executionState: this.state, + currentNodeId, + loopScope, + } + + const language = (block.config?.params as Record | undefined)?.language as + | string + | undefined + + let replacementError: Error | null = null + + let result = replaceValidReferences(template, (match, index) => { + if (replacementError) return match + + try { + if (this.blockResolver.canResolve(match)) { + const resolved = this.resolveReference(match, resolutionContext) + if (resolved === undefined) return match + + const effectiveValue = resolved === RESOLVED_EMPTY ? null : resolved + + // Block output: store in contextVarAccumulator and replace the reference + // with language-specific runtime access to that stored value. + const varName = `__blockRef_${Object.keys(contextVarAccumulator).length}` + contextVarAccumulator[varName] = effectiveValue + const replacement = this.formatContextVariableReference( + varName, + language, + template, + index, + effectiveValue + ) + return replacement + } + + const resolved = this.resolveReference(match, resolutionContext) + if (resolved === undefined) return match + + const effectiveValue = resolved === RESOLVED_EMPTY ? null : resolved + + // Non-block reference (loop, parallel, workflow, env): embed as literal + return this.blockResolver.formatValueForBlock(effectiveValue, BlockType.FUNCTION, language) + } catch (error) { + replacementError = error instanceof Error ? error : new Error(String(error)) + return match + } + }) + + if (replacementError !== null) { + throw replacementError + } + + result = result.replace(createEnvVarPattern(), (match) => { + const resolved = this.resolveReference(match, resolutionContext) + return typeof resolved === 'string' ? resolved : match + }) + + return result + } + + private formatContextVariableReference( + varName: string, + language: string | undefined, + template: string, + matchIndex: number, + value: unknown + ): string { + if (language === 'python') { + return `globals()[${JSON.stringify(varName)}]` + } + + if (language === 'shell') { + return this.formatShellContextVariableReference(varName, template, matchIndex, value) + } + + return `globalThis[${JSON.stringify(varName)}]` + } + + private formatShellContextVariableReference( + varName: string, + template: string, + matchIndex: number, + value: unknown + ): string { + const expansion = `\${${varName}}` + const quoteContext = this.getShellQuoteContext(template, matchIndex) + if (quoteContext === 'double') { + return expansion + } + + const shouldQuote = + quoteContext === 'single' || + typeof value === 'string' || + (typeof value === 'object' && value !== null) || + Array.isArray(value) + + if (!shouldQuote) { + return expansion + } + + const quotedExpansion = `"${expansion}"` + if (quoteContext === 'single') { + return `'${quotedExpansion}'` + } + + return quotedExpansion + } + + private getShellQuoteContext(template: string, index: number): ShellQuoteContext { + let quoteContext: ShellQuoteContext = null + + for (let i = 0; i < index; i++) { + const char = template[i] + + if (quoteContext === null && this.isShellCommentStart(template, i)) { + const nextNewline = template.indexOf('\n', i + 1) + if (nextNewline === -1 || nextNewline >= index) { + break + } + i = nextNewline + continue + } + + if (char === '\\' && quoteContext !== 'single') { + i++ + continue + } + + if (char === "'" && quoteContext !== 'double') { + quoteContext = quoteContext === 'single' ? null : 'single' + } else if (char === '"' && quoteContext !== 'single') { + quoteContext = quoteContext === 'double' ? null : 'double' + } + } + + return quoteContext + } + + private isShellCommentStart(template: string, index: number): boolean { + if (template[index] !== '#') { + return false + } + + const previous = template[index - 1] + return previous === undefined || /\s|[;&|()<>]/.test(previous) + } + private resolveTemplate( ctx: ExecutionContext, currentNodeId: string, diff --git a/apps/sim/lib/api/contracts/hotspots.ts b/apps/sim/lib/api/contracts/hotspots.ts index 08baa37b499..c4e7a277303 100644 --- a/apps/sim/lib/api/contracts/hotspots.ts +++ b/apps/sim/lib/api/contracts/hotspots.ts @@ -99,6 +99,7 @@ export const functionExecuteContract = defineRouteContract({ blockNameMapping: z.record(z.string(), z.string()).optional().default({}), blockOutputSchemas: z.record(z.string(), unknownRecordSchema).optional().default({}), workflowVariables: unknownRecordSchema.optional().default({}), + contextVariables: unknownRecordSchema.optional().default({}), workflowId: z.string().optional(), workspaceId: z.string().optional(), userId: z.string().optional(), diff --git a/apps/sim/tools/function/execute.test.ts b/apps/sim/tools/function/execute.test.ts index e1a966fe94d..73eb21de9e6 100644 --- a/apps/sim/tools/function/execute.test.ts +++ b/apps/sim/tools/function/execute.test.ts @@ -56,10 +56,17 @@ describe('Function Execute Tool', () => { blockData: {}, blockNameMapping: {}, blockOutputSchemas: {}, + contextVariables: {}, isCustomTool: false, language: 'javascript', + outputFormat: undefined, + outputMimeType: undefined, + outputPath: undefined, + outputSandboxPath: undefined, + outputTable: undefined, timeout: 5000, workflowId: undefined, + workspaceId: undefined, userId: undefined, }) }) @@ -85,9 +92,16 @@ describe('Function Execute Tool', () => { blockData: {}, blockNameMapping: {}, blockOutputSchemas: {}, + contextVariables: {}, isCustomTool: false, language: 'javascript', + outputFormat: undefined, + outputMimeType: undefined, + outputPath: undefined, + outputSandboxPath: undefined, + outputTable: undefined, workflowId: undefined, + workspaceId: undefined, userId: undefined, }) }) @@ -105,9 +119,16 @@ describe('Function Execute Tool', () => { blockData: {}, blockNameMapping: {}, blockOutputSchemas: {}, + contextVariables: {}, isCustomTool: false, language: 'javascript', + outputFormat: undefined, + outputMimeType: undefined, + outputPath: undefined, + outputSandboxPath: undefined, + outputTable: undefined, workflowId: undefined, + workspaceId: undefined, userId: undefined, }) }) diff --git a/apps/sim/tools/function/execute.ts b/apps/sim/tools/function/execute.ts index 844b1c6d515..59873843cbb 100644 --- a/apps/sim/tools/function/execute.ts +++ b/apps/sim/tools/function/execute.ts @@ -128,6 +128,7 @@ export const functionExecuteTool: ToolConfig blockNameMapping?: Record blockOutputSchemas?: Record> + /** Pre-resolved block output variables from the executor, injected as VM globals. */ + contextVariables?: Record _context?: { workflowId?: string userId?: string diff --git a/bun.lock b/bun.lock index cd73fd610fd..979cb1eb048 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", From 1989a12ff3d6b840a2906ad83bb7cb45a6c89ff4 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 6 May 2026 10:35:38 -0700 Subject: [PATCH 02/17] improvement(func-exec): normalize inputs to match schema (#4473) --- apps/sim/executor/execution/block-executor.ts | 3 +- apps/sim/executor/execution/executor.ts | 8 +- .../executor/execution/snapshot-serializer.ts | 4 +- apps/sim/executor/execution/snapshot.test.ts | 30 +++++++ apps/sim/executor/execution/snapshot.ts | 30 +++++-- .../executor/handlers/agent/agent-handler.ts | 9 +- .../handlers/condition/condition-handler.ts | 5 +- .../function/function-handler.test.ts | 22 +++++ .../handlers/function/function-handler.ts | 12 ++- apps/sim/lib/core/utils/arrays.test.ts | 13 +++ apps/sim/lib/core/utils/arrays.ts | 10 +++ apps/sim/lib/core/utils/records.test.ts | 64 +++++++++++++ apps/sim/lib/core/utils/records.ts | 90 +++++++++++++++++++ .../lib/workflows/executor/execution-core.ts | 21 ++--- .../executor/queued-workflow-execution.ts | 2 +- apps/sim/providers/utils.ts | 19 +++- apps/sim/tools/function/execute.ts | 18 ++-- apps/sim/tools/utils.ts | 15 ++-- 18 files changed, 325 insertions(+), 50 deletions(-) create mode 100644 apps/sim/executor/execution/snapshot.test.ts create mode 100644 apps/sim/lib/core/utils/arrays.test.ts create mode 100644 apps/sim/lib/core/utils/arrays.ts create mode 100644 apps/sim/lib/core/utils/records.test.ts create mode 100644 apps/sim/lib/core/utils/records.ts diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 73cdcb8d674..3803e53ffd6 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -1,6 +1,7 @@ import { createLogger, type Logger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { redactApiKeys } from '@/lib/core/security/redaction' +import { normalizeStringArray } from '@/lib/core/utils/arrays' import { getBaseUrl } from '@/lib/core/utils/urls' import { containsUserFileWithMetadata, @@ -164,7 +165,7 @@ export class BlockExecutor { block, streamingExec, resolvedInputs, - ctx.selectedOutputs ?? [] + normalizeStringArray(ctx.selectedOutputs) ) } diff --git a/apps/sim/executor/execution/executor.ts b/apps/sim/executor/execution/executor.ts index 8e3a8c8c8c9..a141e017fb1 100644 --- a/apps/sim/executor/execution/executor.ts +++ b/apps/sim/executor/execution/executor.ts @@ -1,4 +1,6 @@ import { createLogger, type Logger } from '@sim/logger' +import { normalizeStringArray } from '@/lib/core/utils/arrays' +import { normalizeStringRecord, normalizeWorkflowVariables } from '@/lib/core/utils/records' import { StartBlockPath } from '@/lib/workflows/triggers/triggers' import type { DAG } from '@/executor/dag/builder' import { DAGBuilder } from '@/executor/dag/builder' @@ -56,9 +58,9 @@ export class DAGExecutor { constructor(options: DAGExecutorOptions) { this.workflow = options.workflow - this.environmentVariables = options.envVarValues ?? {} + this.environmentVariables = normalizeStringRecord(options.envVarValues) this.workflowInput = options.workflowInput ?? {} - this.workflowVariables = options.workflowVariables ?? {} + this.workflowVariables = normalizeWorkflowVariables(options.workflowVariables) this.contextExtensions = options.contextExtensions ?? {} this.dagBuilder = new DAGBuilder() this.execLogger = logger.withMetadata({ @@ -325,7 +327,7 @@ export class DAGExecutor { : new Set(), workflow: this.workflow, stream: this.contextExtensions.stream ?? false, - selectedOutputs: this.contextExtensions.selectedOutputs ?? [], + selectedOutputs: normalizeStringArray(this.contextExtensions.selectedOutputs), edges: this.contextExtensions.edges ?? [], onStream: this.contextExtensions.onStream, onBlockStart: this.contextExtensions.onBlockStart, diff --git a/apps/sim/executor/execution/snapshot-serializer.ts b/apps/sim/executor/execution/snapshot-serializer.ts index 052d4b284b3..76c2a3dba5f 100644 --- a/apps/sim/executor/execution/snapshot-serializer.ts +++ b/apps/sim/executor/execution/snapshot-serializer.ts @@ -119,8 +119,8 @@ export function serializePauseSnapshot( executionMetadata, context.workflow, {}, - context.workflowVariables ?? {}, - context.selectedOutputs ?? [], + context.workflowVariables, + context.selectedOutputs, state ) diff --git a/apps/sim/executor/execution/snapshot.test.ts b/apps/sim/executor/execution/snapshot.test.ts new file mode 100644 index 00000000000..23c7d110cdf --- /dev/null +++ b/apps/sim/executor/execution/snapshot.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import { ExecutionSnapshot } from '@/executor/execution/snapshot' +import type { ExecutionMetadata } from '@/executor/execution/types' + +const metadata: ExecutionMetadata = { + requestId: 'request-1', + executionId: 'execution-1', + workflowId: 'workflow-1', + workspaceId: 'workspace-1', + userId: 'user-1', + triggerType: 'manual', + startTime: '2026-05-06T00:00:00.000Z', +} + +describe('ExecutionSnapshot', () => { + it('normalizes untyped persisted execution state at construction', () => { + const variable = { id: 'var-1', name: 'brand', type: 'plain', value: 'myfitness' } + + const snapshot = new ExecutionSnapshot( + metadata, + { blocks: [] }, + {}, + [variable], + ['agent.content', 123, 'function.result'] + ) + + expect(snapshot.workflowVariables).toEqual({ 'var-1': variable }) + expect(snapshot.selectedOutputs).toEqual(['agent.content', 'function.result']) + }) +}) diff --git a/apps/sim/executor/execution/snapshot.ts b/apps/sim/executor/execution/snapshot.ts index afe9bf52d7f..6e372f97d73 100644 --- a/apps/sim/executor/execution/snapshot.ts +++ b/apps/sim/executor/execution/snapshot.ts @@ -1,14 +1,30 @@ +import { normalizeStringArray } from '@/lib/core/utils/arrays' +import { normalizeWorkflowVariables } from '@/lib/core/utils/records' import type { ExecutionMetadata, SerializableExecutionState } from '@/executor/execution/types' export class ExecutionSnapshot { + public readonly metadata: ExecutionMetadata + public readonly workflow: any + public readonly input: any + public readonly workflowVariables: Record + public readonly selectedOutputs: string[] + public readonly state?: SerializableExecutionState + constructor( - public readonly metadata: ExecutionMetadata, - public readonly workflow: any, - public readonly input: any, - public readonly workflowVariables: Record, - public readonly selectedOutputs: string[] = [], - public readonly state?: SerializableExecutionState - ) {} + metadata: ExecutionMetadata, + workflow: any, + input: any, + workflowVariables: unknown, + selectedOutputs: unknown = [], + state?: SerializableExecutionState + ) { + this.metadata = metadata + this.workflow = workflow + this.input = input + this.workflowVariables = normalizeWorkflowVariables(workflowVariables) + this.selectedOutputs = normalizeStringArray(selectedOutputs) + this.state = state + } toJSON(): string { return JSON.stringify({ diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index b0384a0b7c4..e09b75387f7 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { sleep } from '@sim/utils/helpers' import { and, eq, inArray, isNull } from 'drizzle-orm' +import { normalizeStringRecord, normalizeWorkflowVariables } from '@/lib/core/utils/records' import { createMcpToolId } from '@/lib/mcp/utils' import { getCustomToolById } from '@/lib/workflows/custom-tools/operations' import { getAllBlocks } from '@/blocks' @@ -815,8 +816,8 @@ export class AgentBlockHandler implements BlockHandler { userId: ctx.userId, stream: streaming, messages: messages?.map(({ executionId, ...msg }) => msg), - environmentVariables: ctx.environmentVariables || {}, - workflowVariables: ctx.workflowVariables || {}, + environmentVariables: normalizeStringRecord(ctx.environmentVariables), + workflowVariables: normalizeWorkflowVariables(ctx.workflowVariables), blockData, blockNameMapping, reasoningEffort: inputs.reasoningEffort, @@ -885,8 +886,8 @@ export class AgentBlockHandler implements BlockHandler { userId: ctx.userId, stream: providerRequest.stream, messages: 'messages' in providerRequest ? providerRequest.messages : undefined, - environmentVariables: ctx.environmentVariables || {}, - workflowVariables: ctx.workflowVariables || {}, + environmentVariables: normalizeStringRecord(ctx.environmentVariables), + workflowVariables: normalizeWorkflowVariables(ctx.workflowVariables), blockData, blockNameMapping, isDeployedContext: ctx.isDeployedContext, diff --git a/apps/sim/executor/handlers/condition/condition-handler.ts b/apps/sim/executor/handlers/condition/condition-handler.ts index 60ad9f99860..9fd4dd50f3c 100644 --- a/apps/sim/executor/handlers/condition/condition-handler.ts +++ b/apps/sim/executor/handlers/condition/condition-handler.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { normalizeStringRecord, normalizeWorkflowVariables } from '@/lib/core/utils/records' import type { BlockOutput } from '@/blocks/types' import { BlockType, CONDITION, DEFAULTS, EDGE } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' @@ -40,8 +41,8 @@ export async function evaluateConditionExpression( { code, timeout: CONDITION_TIMEOUT_MS, - envVars: ctx.environmentVariables || {}, - workflowVariables: ctx.workflowVariables || {}, + envVars: normalizeStringRecord(ctx.environmentVariables), + workflowVariables: normalizeWorkflowVariables(ctx.workflowVariables), blockData, blockNameMapping, blockOutputSchemas, diff --git a/apps/sim/executor/handlers/function/function-handler.test.ts b/apps/sim/executor/handlers/function/function-handler.test.ts index 384540cc7b2..d7a4e19929f 100644 --- a/apps/sim/executor/handlers/function/function-handler.test.ts +++ b/apps/sim/executor/handlers/function/function-handler.test.ts @@ -196,6 +196,28 @@ describe('FunctionBlockHandler', () => { ) }) + it('should normalize malformed execution context records before calling function_execute', async () => { + const legacyVariable = { id: 'var-1', name: 'brand', type: 'plain', value: 'myfitness' } + mockContext.workflowVariables = [legacyVariable] as unknown as Record + mockContext.environmentVariables = ['invalid-env'] as unknown as Record + + await handler.execute(mockContext, mockBlock, { + code: 'return "myfitness"', + [FUNCTION_BLOCK_CONTEXT_VARS_KEY]: ['invalid-context'], + }) + + expect(mockExecuteTool).toHaveBeenCalledWith( + 'function_execute', + expect.objectContaining({ + envVars: {}, + workflowVariables: { 'var-1': legacyVariable }, + contextVariables: {}, + }), + false, + mockContext + ) + }) + it('should handle tool error with no specific message', async () => { const inputs = { code: 'some code' } const errorResult = { success: false } diff --git a/apps/sim/executor/handlers/function/function-handler.ts b/apps/sim/executor/handlers/function/function-handler.ts index c008a8d07ea..5cd01e1aa1f 100644 --- a/apps/sim/executor/handlers/function/function-handler.ts +++ b/apps/sim/executor/handlers/function/function-handler.ts @@ -1,3 +1,8 @@ +import { + normalizeRecord, + normalizeStringRecord, + normalizeWorkflowVariables, +} from '@/lib/core/utils/records' import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants' import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages' import { BlockType } from '@/executor/constants' @@ -26,8 +31,7 @@ export class FunctionBlockHandler implements BlockHandler { const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx) - const contextVariables = - (inputs[FUNCTION_BLOCK_CONTEXT_VARS_KEY] as Record | undefined) ?? {} + const contextVariables = normalizeRecord(inputs[FUNCTION_BLOCK_CONTEXT_VARS_KEY]) const result = await executeTool( 'function_execute', @@ -35,8 +39,8 @@ export class FunctionBlockHandler implements BlockHandler { code: codeContent, language: inputs.language || DEFAULT_CODE_LANGUAGE, timeout: inputs.timeout || DEFAULT_EXECUTION_TIMEOUT_MS, - envVars: ctx.environmentVariables || {}, - workflowVariables: ctx.workflowVariables || {}, + envVars: normalizeStringRecord(ctx.environmentVariables), + workflowVariables: normalizeWorkflowVariables(ctx.workflowVariables), blockData, blockNameMapping, blockOutputSchemas, diff --git a/apps/sim/lib/core/utils/arrays.test.ts b/apps/sim/lib/core/utils/arrays.test.ts new file mode 100644 index 00000000000..54062fa7f8d --- /dev/null +++ b/apps/sim/lib/core/utils/arrays.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest' +import { normalizeStringArray } from '@/lib/core/utils/arrays' + +describe('array normalization utilities', () => { + it('normalizes string arrays loaded from untyped state', () => { + expect(normalizeStringArray(['output-1', 2, 'output-2', null])).toEqual([ + 'output-1', + 'output-2', + ]) + expect(normalizeStringArray('output-1')).toEqual([]) + expect(normalizeStringArray(undefined)).toEqual([]) + }) +}) diff --git a/apps/sim/lib/core/utils/arrays.ts b/apps/sim/lib/core/utils/arrays.ts new file mode 100644 index 00000000000..ffc270d6081 --- /dev/null +++ b/apps/sim/lib/core/utils/arrays.ts @@ -0,0 +1,10 @@ +/** + * Normalizes optional string-list values loaded from untyped persisted state. + */ +export function normalizeStringArray(value: unknown): string[] { + if (!Array.isArray(value)) { + return [] + } + + return value.filter((item): item is string => typeof item === 'string') +} diff --git a/apps/sim/lib/core/utils/records.test.ts b/apps/sim/lib/core/utils/records.test.ts new file mode 100644 index 00000000000..97a46f0e5dc --- /dev/null +++ b/apps/sim/lib/core/utils/records.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest' +import { + isPlainRecord, + normalizeRecord, + normalizeRecordMap, + normalizeStringRecord, + normalizeWorkflowVariables, +} from '@/lib/core/utils/records' + +describe('record normalization utilities', () => { + it('identifies plain records without accepting arrays or null', () => { + expect(isPlainRecord({})).toBe(true) + expect(isPlainRecord(Object.create(null))).toBe(true) + expect(isPlainRecord([])).toBe(false) + expect(isPlainRecord(null)).toBe(false) + }) + + it('normalizes unknown values to object records', () => { + expect(normalizeRecord({ value: 1 })).toEqual({ value: 1 }) + expect(normalizeRecord([])).toEqual({}) + expect(normalizeRecord('not-a-record')).toEqual({}) + }) + + it('normalizes string records for environment-like values', () => { + expect( + normalizeStringRecord({ + TOKEN: 'secret', + RETRIES: 3, + ENABLED: true, + EMPTY: null, + }) + ).toEqual({ + TOKEN: 'secret', + RETRIES: '3', + ENABLED: 'true', + }) + expect(normalizeStringRecord([])).toEqual({}) + }) + + it('normalizes record maps by dropping malformed entries', () => { + expect( + normalizeRecordMap({ + valid: { type: 'string' }, + invalid: [], + }) + ).toEqual({ + valid: { type: 'string' }, + }) + }) + + it('normalizes legacy workflow variable arrays into records', () => { + const variableWithId = { id: 'var-1', name: 'brand', type: 'plain', value: 'myfitness' } + const variableWithName = { name: 'channel', type: 'plain', value: 'whatsapp' } + + expect(normalizeWorkflowVariables([variableWithId, variableWithName, []])).toEqual({ + 'var-1': variableWithId, + channel: variableWithName, + }) + expect(normalizeWorkflowVariables({ existing: variableWithId })).toEqual({ + existing: variableWithId, + }) + expect(normalizeWorkflowVariables('not-a-record')).toEqual({}) + }) +}) diff --git a/apps/sim/lib/core/utils/records.ts b/apps/sim/lib/core/utils/records.ts new file mode 100644 index 00000000000..f5e0c4842ca --- /dev/null +++ b/apps/sim/lib/core/utils/records.ts @@ -0,0 +1,90 @@ +export type UnknownRecord = Record +export type StringRecord = Record + +/** + * Returns true only for object-map values, excluding arrays and null. + */ +export function isPlainRecord(value: unknown): value is UnknownRecord { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return false + } + + const prototype = Object.getPrototypeOf(value) + return prototype === Object.prototype || prototype === null +} + +/** + * Normalizes optional execution context maps to the record shape expected by + * internal API contracts. + */ +export function normalizeRecord(value: unknown): UnknownRecord { + return isPlainRecord(value) ? value : {} +} + +/** + * Normalizes environment-like maps to string values, matching process/env + * semantics at execution boundaries. + */ +export function normalizeStringRecord(value: unknown): StringRecord { + if (!isPlainRecord(value)) { + return {} + } + + const normalized: StringRecord = {} + for (const [key, entryValue] of Object.entries(value)) { + if (entryValue === undefined || entryValue === null) { + continue + } + normalized[key] = typeof entryValue === 'string' ? entryValue : String(entryValue) + } + return normalized +} + +/** + * Normalizes record-of-record maps such as block output schema maps. + */ +export function normalizeRecordMap(value: unknown): Record { + if (!isPlainRecord(value)) { + return {} + } + + const normalized: Record = {} + for (const [key, entryValue] of Object.entries(value)) { + if (isPlainRecord(entryValue)) { + normalized[key] = entryValue + } + } + return normalized +} + +/** + * Workflow variables are stored as a record in current state, while some + * legacy and imported snapshots can carry an array of variable objects. + */ +export function normalizeWorkflowVariables(value: unknown): UnknownRecord { + if (isPlainRecord(value)) { + return value + } + + if (!Array.isArray(value)) { + return {} + } + + const normalized: UnknownRecord = {} + for (const variable of value) { + if (!isPlainRecord(variable)) { + continue + } + + const id = typeof variable.id === 'string' && variable.id.trim() ? variable.id : undefined + const name = + typeof variable.name === 'string' && variable.name.trim() ? variable.name : undefined + const key = id ?? name + + if (key) { + normalized[key] = variable + } + } + + return normalized +} diff --git a/apps/sim/lib/workflows/executor/execution-core.ts b/apps/sim/lib/workflows/executor/execution-core.ts index aec5f956c50..22b58c5e707 100644 --- a/apps/sim/lib/workflows/executor/execution-core.ts +++ b/apps/sim/lib/workflows/executor/execution-core.ts @@ -7,6 +7,7 @@ import { createLogger } from '@sim/logger' import { mergeSubblockStateWithValues } from '@sim/workflow-persistence/subblocks' import type { Edge } from 'reactflow' import { z } from 'zod' +import { isPlainRecord } from '@/lib/core/utils/records' import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils' import { clearExecutionCancellation } from '@/lib/execution/cancellation' import type { LoggingSession } from '@/lib/logs/execution/logging-session' @@ -581,6 +582,16 @@ export async function executeWorkflowCore( callChain: metadata.callChain, } + for (const variable of Object.values(workflowVariables)) { + if ( + isPlainRecord(variable) && + variable.value !== undefined && + typeof variable.type === 'string' + ) { + variable.value = parseVariableValueByType(variable.value, variable.type) + } + } + const executorInstance = new Executor({ workflow: serializedWorkflow, envVarValues: decryptedEnvVars, @@ -589,16 +600,6 @@ export async function executeWorkflowCore( contextExtensions, }) - // Convert initial workflow variables to their native types - if (workflowVariables) { - for (const [varId, variable] of Object.entries(workflowVariables)) { - const v = variable as { value?: unknown; type?: string } - if (v.value !== undefined && v.type) { - v.value = parseVariableValueByType(v.value, v.type) - } - } - } - const result = runFromBlock ? ((await executorInstance.executeFromBlock( workflowId, diff --git a/apps/sim/lib/workflows/executor/queued-workflow-execution.ts b/apps/sim/lib/workflows/executor/queued-workflow-execution.ts index 06a851a3b53..0106e823195 100644 --- a/apps/sim/lib/workflows/executor/queued-workflow-execution.ts +++ b/apps/sim/lib/workflows/executor/queued-workflow-execution.ts @@ -138,7 +138,7 @@ export async function executeQueuedWorkflowJob( payload.workflow, payload.input, payload.variables, - payload.selectedOutputs ?? [] + payload.selectedOutputs ) let callbacks = {} diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 0d5b7be022f..780776aed36 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -5,6 +5,11 @@ import type { CompletionUsage } from 'openai/resources/completions' import { dollarsToCredits } from '@/lib/billing/credits/conversion' import { env } from '@/lib/core/config/env' import { getBlacklistedProvidersFromEnv, isHosted } from '@/lib/core/config/feature-flags' +import { + normalizeRecord, + normalizeStringRecord, + normalizeWorkflowVariables, +} from '@/lib/core/utils/records' import { buildCanonicalIndex, type CanonicalGroup, @@ -1166,10 +1171,16 @@ export function prepareToolExecution( }, } : {}), - ...(request.environmentVariables ? { envVars: request.environmentVariables } : {}), - ...(request.workflowVariables ? { workflowVariables: request.workflowVariables } : {}), - ...(request.blockData ? { blockData: request.blockData } : {}), - ...(request.blockNameMapping ? { blockNameMapping: request.blockNameMapping } : {}), + ...(request.environmentVariables + ? { envVars: normalizeStringRecord(request.environmentVariables) } + : {}), + ...(request.workflowVariables + ? { workflowVariables: normalizeWorkflowVariables(request.workflowVariables) } + : {}), + ...(request.blockData ? { blockData: normalizeRecord(request.blockData) } : {}), + ...(request.blockNameMapping + ? { blockNameMapping: normalizeStringRecord(request.blockNameMapping) } + : {}), ...(tool.parameters ? { _toolSchema: tool.parameters } : {}), } diff --git a/apps/sim/tools/function/execute.ts b/apps/sim/tools/function/execute.ts index 59873843cbb..7ee26f5c4b3 100644 --- a/apps/sim/tools/function/execute.ts +++ b/apps/sim/tools/function/execute.ts @@ -1,3 +1,9 @@ +import { + normalizeRecord, + normalizeRecordMap, + normalizeStringRecord, + normalizeWorkflowVariables, +} from '@/lib/core/utils/records' import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants' import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages' import type { CodeExecutionInput, CodeExecutionOutput } from '@/tools/function/types' @@ -123,12 +129,12 @@ export const functionExecuteTool: ToolConfig Date: Wed, 6 May 2026 10:38:34 -0700 Subject: [PATCH 03/17] feat(models): add grok-4.3 (#4472) --- apps/sim/providers/models.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index 83ba847f77e..e75569c6486 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -1599,6 +1599,21 @@ export const PROVIDER_DEFINITIONS: Record = { toolUsageControl: true, }, models: [ + { + id: 'grok-4.3', + pricing: { + input: 1.25, + cachedInput: 0.2, + output: 2.5, + updatedAt: '2026-05-05', + }, + capabilities: { + temperature: { min: 0, max: 1 }, + }, + contextWindow: 1000000, + releaseDate: '2026-04-30', + recommended: true, + }, { id: 'grok-4-latest', pricing: { From 5d38222a14c3a443c034497efa7a7648998a2aa9 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 6 May 2026 10:57:35 -0700 Subject: [PATCH 04/17] fix(function): validate custom tool param keys before code interpolation (#4474) * fix(function): validate custom tool param keys before code interpolation * fix(function): exclude JS reserved words from param key injection guard --- apps/sim/app/api/function/execute/route.ts | 63 +++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index bd18d5686cd..c45a4bef7b7 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -34,6 +34,59 @@ const TAG_PATTERN = createReferencePattern() const E2B_JS_WRAPPER_LINES = 3 const E2B_PYTHON_WRAPPER_LINES = 1 +/** Matches valid JS identifier names (letters, digits, underscore; no leading digit). */ +const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/ + +/** ES2023 reserved words — using these as `const` variable names produces a SyntaxError. */ +const JS_RESERVED_WORDS = new Set([ + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'function', + 'if', + 'import', + 'in', + 'instanceof', + 'let', + 'new', + 'null', + 'return', + 'static', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'yield', + 'enum', + 'await', + 'implements', + 'interface', + 'package', + 'private', + 'protected', + 'public', +]) + type TypeScriptModule = typeof import('typescript') let typescriptModulePromise: Promise | null = null @@ -1089,10 +1142,16 @@ export const POST = withRouteHandler(async (req: NextRequest) => { const executionMethod = 'isolated-vm' + const isSafeParamKey = (key: string) => SAFE_IDENTIFIER.test(key) && !JS_RESERVED_WORDS.has(key) + const wrapperLines = ['(async () => {', ' try {'] if (isCustomTool) { Object.keys(executionParams).forEach((key) => { - wrapperLines.push(` const ${key} = params.${key};`) + if (isSafeParamKey(key)) { + wrapperLines.push(` const ${key} = params.${key};`) + } else { + logger.warn('Skipping param key — not a safe JS identifier', { key, requestId }) + } }) } userCodeStartLine = wrapperLines.length + 1 @@ -1100,7 +1159,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { let codeToExecute = resolvedCode let prependedLineCount = 0 if (isCustomTool) { - const paramKeys = Object.keys(executionParams) + const paramKeys = Object.keys(executionParams).filter(isSafeParamKey) const paramDestructuring = paramKeys.map((key) => `const ${key} = params.${key};`).join('\n') codeToExecute = `${paramDestructuring}\n${resolvedCode}` prependedLineCount = paramKeys.length From ad88859d6e2c1adffef4c175e67f3059219c7141 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 6 May 2026 10:57:47 -0700 Subject: [PATCH 05/17] chore(skills): add /add-model and /validate-model commands (#4475) --- .claude/commands/add-model.md | 159 +++++++++++++++++++++++++++ .claude/commands/validate-model.md | 166 +++++++++++++++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 .claude/commands/add-model.md create mode 100644 .claude/commands/validate-model.md diff --git a/.claude/commands/add-model.md b/.claude/commands/add-model.md new file mode 100644 index 00000000000..1fcf828537c --- /dev/null +++ b/.claude/commands/add-model.md @@ -0,0 +1,159 @@ +--- +description: Add a new LLM model to apps/sim/providers/models.ts with specs verified against the provider's live API docs (no hallucination) +argument-hint: [docs-url] +--- + +# Add Model Skill + +You add a new model entry to `apps/sim/providers/models.ts`. **Every numeric and capability claim MUST be derived from a live web fetch of the provider's official docs in this session.** Marketing emails, training data, and your prior knowledge are not sources of truth — they routinely hallucinate pricing, context windows, and capability lists. + +## Hard rules (do not skip) + +1. **Live-fetch or refuse.** Before writing the entry, you must successfully WebFetch the provider's official models/pricing page in this session. If you cannot reach an authoritative source for any field, **mark the field as UNVERIFIED in your report and ask the user before guessing**. Never fill in pricing or capabilities from memory. +2. **Two-source rule for pricing.** Cross-check input/output/cached pricing against at least one secondary source (OpenRouter, Artificial Analysis, CloudPrice, mem0, intuitionlabs). If sources disagree, the provider's own docs win — but flag the disagreement. +3. **Read the code before setting capability flags.** Capability flags are dead unless the provider's implementation under `apps/sim/providers/{provider}/` actually consumes them (see Consumption Matrix below). Setting a flag the provider ignores is a silent bug. +4. **Cite every fact.** Your final report must list the URL each value came from. No URL → not verified. + +## Your Task + +1. Identify provider and model id from user args +2. Live-fetch official docs + pricing page + capability/parameter pages + at least one secondary source +3. Apply the Consumption Matrix to know which capability flags are real +4. Read 2-3 sibling entries in `models.ts` and match their pattern exactly +5. Insert the entry, run `bun run lint`, print the verification report + +## Step 1: Live source-of-truth lookup + +In priority order — fetch all that exist for the provider: + +| Provider | Models index | Pricing | Reasoning/parameter caveats | +|---|---|---|---| +| OpenAI | platform.openai.com/docs/models | openai.com/api/pricing | platform.openai.com/docs/guides/reasoning | +| Anthropic | docs.anthropic.com/en/docs/about-claude/models | anthropic.com/pricing | docs.anthropic.com/en/docs/build-with-claude/extended-thinking | +| Google (Gemini) | ai.google.dev/gemini-api/docs/models | ai.google.dev/pricing | ai.google.dev/gemini-api/docs/thinking | +| xAI | docs.x.ai/developers/models | docs.x.ai/developers/models (per-model detail page) | docs.x.ai/developers/model-capabilities/text/reasoning | +| Mistral | docs.mistral.ai/getting-started/models/models_overview | mistral.ai/pricing | n/a | +| DeepSeek | api-docs.deepseek.com/quick_start/pricing | same | api-docs.deepseek.com/guides/reasoning_model | +| Groq | console.groq.com/docs/models | groq.com/pricing | n/a | +| Cerebras | inference-docs.cerebras.ai/models | cerebras.ai/pricing | n/a | + +Secondary verification (use at least one): `openrouter.ai//`, `artificialanalysis.ai/models/`, `cloudprice.net/models/-`. + +Use a precise WebFetch prompt: *"Extract for {model_id}: exact model id string, context window in tokens, input price per 1M, cached input price per 1M, output price per 1M, max output tokens, supported reasoning effort levels, accepted parameters (temperature, top_p), release date. Do not fill in fields you cannot find."* + +## Step 2: Consumption Matrix (which provider honors which capability) + +| Capability | Honored by | Effect if set elsewhere | +|---|---|---| +| `temperature` | All providers (passed through if set) | Safe but inert on always-reasoning models that reject it | +| `toolUsageControl` | All providers (provider-level, not per-model) | n/a — set on `ProviderDefinition`, not models | +| `reasoningEffort` | `openai/core.ts`, `azure-openai`, `anthropic/core.ts` (mapped to thinking), `gemini/core.ts` | **Dead on xai, deepseek, mistral, groq, cerebras, openrouter, fireworks, bedrock, vertex** unless their core consumes it — re-grep before assuming | +| `verbosity` | `openai/core.ts`, `azure-openai/index.ts` only | Dead elsewhere | +| `thinking` | `anthropic/core.ts`, `gemini/core.ts` | Dead elsewhere | +| `nativeStructuredOutputs` | `anthropic/core.ts`, `fireworks/index.ts`, `openrouter/index.ts` | Dead on openai, xai, google, vertex, bedrock, azure-openai, deepseek, mistral, groq, cerebras | +| `maxOutputTokens` | Read by UI + executor for token estimation | Always meaningful — set if provider documents a cap | +| `computerUse` | `anthropic/core.ts` | Dead elsewhere | +| `deepResearch` | UI flag for routing to deep-research SKUs | Set only on actual deep-research model IDs | +| `memory: false` | Conversation persistence opt-out | Set only when model genuinely cannot maintain history (e.g., deep-research) | + +**Always re-grep before relying on this table** — the codebase moves: + +```bash +rg "reasoningEffort|reasoning_effort" apps/sim/providers// +rg "verbosity" apps/sim/providers// +rg "request\.thinking|thinking:" apps/sim/providers// +rg "supportsNativeStructuredOutputs|nativeStructuredOutputs" apps/sim/providers// +``` + +## Step 3: Match the provider's existing entry pattern + +Open `apps/sim/providers/models.ts`, find `PROVIDER_DEFINITIONS[].models`, read 2-3 sibling entries. Match field order exactly: + +```ts +{ + id: '', + pricing: { + input: , + cachedInput: , // omit if provider doesn't offer caching + output: , + updatedAt: '', + }, + capabilities: { + // only flags the provider actually consumes — see matrix + }, + contextWindow: , + releaseDate: '', + recommended: true, // only if new flagship; ask user before swapping + speedOptimized: true, // only on smallest/fastest tier + deprecated: true, // only on retired models +} +``` + +### Reseller providers (azure-openai, azure-anthropic, vertex, bedrock, openrouter) + +Model id MUST be prefixed: `azure/`, `azure-anthropic/`, `vertex/`, `bedrock/`, `openrouter/`. Pricing usually mirrors the upstream provider but verify on the reseller's own pricing page. + +### Insertion order + +Within a family, newest first (matches existing convention: GPT-5.5 above GPT-5.4 above GPT-5.2). Across families, biggest/flagship at top of list. + +### `recommended` / `speedOptimized` + +- At most one or two `recommended: true` per provider — the current flagship(s). +- If you're adding a new flagship, ask the user before removing `recommended` from the previous flagship. Never silently flip it. +- `speedOptimized: true` only on the smallest/fastest tier (nano, flash-lite, haiku class). + +## Step 4: Write, lint + +```bash +bun run lint +``` + +Lint must pass before reporting done. **If lint fails:** read the error, fix the syntax/typing issue in the entry you just wrote (do not delete the entry — it's the work product), re-run lint, and note the fix in a "Lint adjustments" line in the verification report. Never report done with lint failing. + +## Step 5: Verification report (mandatory format) + +End with this exact structure: + +```markdown +### Verification — + +| Field | Value | Source URL | Status | +|---|---|---|---| +| `id` | `grok-4.3` | https://docs.x.ai/... | ✓ verified | +| `contextWindow` | 1,000,000 | https://docs.x.ai/... + https://openrouter.ai/... | ✓ verified (2 sources agree) | +| `input` | $1.25/M | https://docs.x.ai/... | ✓ verified | +| `cachedInput` | $0.20/M | https://cloudprice.net/... | ⚠️ single source | +| `output` | $2.50/M | https://docs.x.ai/... + https://openrouter.ai/... | ✓ verified | +| `capabilities.temperature` | `{ min: 0, max: 1 }` | matches sibling entries | — pattern-match only | +| `capabilities.reasoningEffort` | NOT SET | provider docs say API rejects it for this model | ✓ correctly omitted | +| `releaseDate` | 2026-04-30 | https://docs.x.ai/... announcement | ✓ verified | + +**Disagreements** +- _none_ OR _OpenRouter says X, provider docs say Y — used Y per provider rule_ + +**Unverified fields** +- _none_ OR _: could not find authoritative source — left as based on sibling pattern; please confirm_ +``` + +If any row is ⚠️ single-source or "unverified," **state it plainly to the user and ask whether to proceed**. Do not silently merge. + +## What to do if you cannot find a source + +Omitting a field is **not the same as verifying it**. Any field you cannot confirm from a live fetch must be **both** omitted from the entry **and** listed as ❓ UNVERIFIED in the report's "Unverified fields" section, with the URLs you attempted. Then ask the user to confirm before merging. + +- Pricing missing → do NOT guess. Omit `cachedInput`. Mark ❓ UNVERIFIED. Ask the user for the price or the docs URL. +- Context window missing → do NOT guess. Ask the user; mark ❓ UNVERIFIED. +- Release date missing → omit the field; mark ❓ UNVERIFIED in the report. +- Capability uncertain → omit the flag (safer than setting a dead/wrong one); mark ❓ UNVERIFIED so the user knows you didn't confirm it either way. + +## Anti-patterns this skill exists to prevent + +- ❌ Trusting a marketing email (xAI's grok-4.3 email claimed "3 reasoning efforts" but the API rejects `reasoning_effort` — verified by official docs only) +- ❌ Setting `nativeStructuredOutputs: true` on xai/openai/google (dead — only anthropic/fireworks/openrouter consume it) +- ❌ Setting `thinking` on non-Anthropic/non-Gemini providers +- ❌ Setting `verbosity` on anything other than OpenAI gpt-5.x +- ❌ Copying `pricing.updatedAt` from a sibling instead of using today's date +- ❌ Inventing a `cachedInput` price by dividing input by 4 (varies by provider — find an explicit number) +- ❌ Stamping `recommended: true` on the new model without removing it from the previous flagship +- ❌ Reporting "done" with any UNVERIFIED row in the table diff --git a/.claude/commands/validate-model.md b/.claude/commands/validate-model.md new file mode 100644 index 00000000000..10c6aaa0b27 --- /dev/null +++ b/.claude/commands/validate-model.md @@ -0,0 +1,166 @@ +--- +description: Validate a model entry (or every model in a provider) in apps/sim/providers/models.ts against the provider's live API docs (no hallucination — reports what cannot be verified) +argument-hint: [model-id] +--- + +# Validate Model Skill + +You audit one or more model entries in `apps/sim/providers/models.ts` against the provider's official live API docs. **Hallucinated pricing and capabilities are the #1 failure mode in this file.** Every numeric and capability claim must be re-derived from a live web fetch in this session — not from memory, not from training data, not from the user's marketing email. + +## Hard rules (do not skip) + +1. **Live-fetch or report unverified.** Each field must be backed by a live WebFetch in this session. If you cannot reach an authoritative URL for a field, mark it **UNVERIFIED** in the report — do not silently confirm it from memory. +2. **Cite every fact.** Every value in the report must show the source URL it was checked against. No URL → mark UNVERIFIED. +3. **Two-source rule for pricing.** Cross-check input/output/cached against at least one secondary source (OpenRouter, Artificial Analysis, CloudPrice). If sources disagree, the provider's own docs win — flag the disagreement. +4. **Inspect provider implementation before flagging capability mismatches.** A capability flag in `models.ts` is dead unless the provider's code under `apps/sim/providers/{provider}/` consumes it (see Consumption Matrix below). Setting a flag the provider ignores is a warning, not a critical. +5. **Never auto-fix without printing the diff.** Show the user the proposed diff before applying. Get confirmation. + +## Your Task + +When invoked as `/validate-model [model-id]`: + +1. Read the target entries from `models.ts` +2. Live-fetch the provider's official models, pricing, and capability/reasoning pages + at least one secondary source for pricing +3. Inspect the provider implementation to know which flags are actually consumed +4. Run the checklist below per model +5. Report findings (critical / warning / suggestion / unverified) with every cell linked to its source URL +6. Offer to fix; on confirm, edit `models.ts` in a single pass and re-lint + +If `model-id` is omitted, validate every model in the provider. + +## Step 1: Read entries from `models.ts` + +Capture per model: `id`, full `pricing`, full `capabilities`, `contextWindow`, `releaseDate`, `recommended`, `speedOptimized`, `deprecated`. + +## Step 2: Live-fetch authoritative sources + +Use the canonical provider URL table in `add-model.md` (Step 1) as the single source of truth — fetch the models index, pricing, and reasoning/parameter caveats pages listed there for the target provider. If you update one table, update the other in the same change. + +Secondary cross-check (use at least one): OpenRouter, Artificial Analysis, CloudPrice. + +If a fetch fails (404, timeout, paywall), record the URL attempted and mark dependent fields UNVERIFIED. + +## Step 3: Build the consumption map for this provider + +Re-grep before trusting the snapshot below: + +```bash +rg "reasoningEffort|reasoning_effort" apps/sim/providers// +rg "verbosity" apps/sim/providers// +rg "request\.thinking|thinking:" apps/sim/providers// +rg "supportsNativeStructuredOutputs|nativeStructuredOutputs" apps/sim/providers// +``` + +Snapshot (verify before relying): + +| Capability | Consumed by | +|---|---| +| `reasoningEffort` | `openai/core.ts`, `azure-openai`, `anthropic/core.ts` (mapped via thinking), `gemini/core.ts` | +| `verbosity` | `openai/core.ts`, `azure-openai/index.ts` | +| `thinking` | `anthropic/core.ts`, `gemini/core.ts` | +| `nativeStructuredOutputs` | `anthropic/core.ts`, `fireworks/index.ts`, `openrouter/index.ts` | +| `computerUse` | `anthropic/core.ts` | +| `temperature` | All providers (passthrough) | + +A flag set in `models.ts` but not in the consumption list for this provider = **warning: dead flag**. + +## Step 4: Run the checklist + +For each model, evaluate every row. Statuses: ✓ matches docs, ✗ disagrees, ⚠️ single-source, ❓ UNVERIFIED (could not fetch). + +### Identity +- [ ] `id` exactly matches provider's API model identifier (case, dots, dashes, prefix for resellers) +- [ ] `releaseDate` matches launch announcement +- [ ] `deprecated: true` set if provider has announced retirement (or removed from active list) + +### Pricing (per 1M tokens, USD) +- [ ] `pricing.input` matches provider pricing page +- [ ] `pricing.output` matches provider pricing page +- [ ] `pricing.cachedInput` matches provider's documented cached/prompt-cache rate (or is correctly omitted if no caching offered) +- [ ] `pricing.updatedAt` is recent — warn if older than 60 days + +### Context & output limits +- [ ] `contextWindow` matches docs (in tokens) +- [ ] `capabilities.maxOutputTokens` matches documented output cap (or is correctly omitted if "no output limit") + +### Capabilities (each must be DOCUMENTED-AS-SUPPORTED **and** CONSUMED-BY-PROVIDER-CODE) +- [ ] `temperature` — provider accepts it for this model (reasoning-always-on models often reject) +- [ ] `reasoningEffort.values` — list matches docs; **omitted** for always-reasoning models that reject the parameter (e.g., grok-4.3, where xAI docs explicitly state `reasoning_effort` is not supported). Verify per model — some always-reasoning models (e.g., OpenAI's o-series) DO accept `reasoning_effort` and should keep the flag. +- [ ] `verbosity.values` — only on OpenAI gpt-5.x family; values match docs +- [ ] `thinking.levels` + `thinking.default` — only on Anthropic/Gemini; values match docs +- [ ] `nativeStructuredOutputs` — only on anthropic/fireworks/openrouter; provider must document Structured Outputs / JSON-mode for this model +- [ ] `toolUsageControl` — provider supports `tool_choice` semantics +- [ ] `computerUse` — provider implements computer-use loop AND model is a computer-use SKU +- [ ] `deepResearch` — only on actual deep-research SKUs +- [ ] `memory: false` — only when the model genuinely cannot maintain conversation history + +### Flags +- [ ] `recommended: true` — at most one or two per provider; should be current flagship +- [ ] `speedOptimized: true` — only on smallest/fastest tier (nano / flash-lite / haiku class) + +## Step 5: Report (mandatory format) + +For each model, emit a table with one row per checklist item. Every row that claims ✓ must have a URL. + +```markdown +### Validation — + +| Field | Repo | Live docs | Source URL | Status | +|---|---|---|---|---| +| `input` | $1.25/M | $1.25/M | https://docs.x.ai/... | ✓ | +| `cachedInput` | $0.50/M | $0.20/M | https://cloudprice.net/... | ✗ stale (price cut not picked up) | +| `reasoningEffort` | low/medium/high | rejected by API | https://docs.x.ai/.../reasoning | ✗ inert — selecting silently no-ops | +| `contextWindow` | 1,000,000 | 1,000,000 | https://docs.x.ai/... + https://openrouter.ai/... | ✓ (2 sources) | +| `releaseDate` | 2026-04-30 | not found in scraped pages | _attempted: docs.x.ai, x.ai/news_ | ❓ UNVERIFIED | + +**Findings** +- 🔴 critical — `cachedInput` is wrong: docs say $0.20/M, repo has $0.50/M +- 🟡 warning — `reasoningEffort` is set but provider rejects it for this model (xAI docs explicitly: "reasoning_effort is not supported by grok-4.3") +- 🔵 suggestion — `pricing.updatedAt` is 90 days old; refresh +- ❓ unverified — `releaseDate` could not be confirmed from any fetched page; ask user + +**Disagreements between sources** +- _none_ OR _OpenRouter says $X, provider docs say $Y — went with provider docs_ +``` + +End each multi-model run with a summary count: `N models checked · X critical · Y warnings · Z suggestions · W unverified`. + +## Step 6: Offer to fix + +After reporting, ask: *"Want me to fix the critical and warning items? I'll print the diff first."* On yes: + +1. Print the proposed diff (do not apply yet) +2. Get user confirmation +3. Edit `models.ts` in a single pass +4. Run `bun run lint` +5. Re-run only the failed rows of the checklist on the new state + +## Severity definitions + +- 🔴 **critical** — wrong number or wrong identifier that misleads users about cost or breaks API calls. Examples: incorrect pricing, wrong model id, wrong context window, capability the API rejects. +- 🟡 **warning** — dead code or internal inconsistency. Examples: capability flag the provider ignores, multiple `recommended: true` per provider, `pricing.updatedAt` >60 days old, missing `deprecated: true` on retired model. +- 🔵 **suggestion** — style/consistency. Examples: field order, missing `speedOptimized` on a clearly smallest-tier model. +- ❓ **unverified** — could not fetch an authoritative source for this field. Surface it; never silently confirm. + +## Common bugs this skill catches + +- Pricing drift after a provider price cut (very common — providers cut quarterly) +- `reasoningEffort` set on always-reasoning models that reject the parameter (grok-4.3, o3-pro pattern) +- `nativeStructuredOutputs` set on providers that don't consume the flag (dead) +- `thinking` set on non-Anthropic/non-Gemini providers +- `verbosity` set on non-gpt-5.x models +- Wrong context window (e.g., 128k claimed vs 200k actual) +- Stale `pricing.updatedAt` +- Multiple `recommended: true` per provider after a flagship swap +- Missing `deprecated: true` on retired models (e.g., the xAI batch retiring May 15, 2026) + +## What "I cannot verify this" looks like + +If, after fetching the documented sources, a field cannot be confirmed: + +- Mark the row ❓ UNVERIFIED with the URL(s) attempted +- Surface it in the **Findings** section with severity ❓ +- Do NOT mark the validation as passed +- Ask the user for a docs URL or guidance before changing anything + +The skill is allowed to say *"I could not verify the cached input price for grok-4.3 from the official xAI docs in this session — I attempted [URLs] without finding the value. Third-party sources [URL1, URL2] both report $0.20/M. Confirm before I update."* That is correct behavior. Hallucinating a number is not. From 48331451b05c7f031dcfb5770f99da13702eed72 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 6 May 2026 11:08:01 -0700 Subject: [PATCH 06/17] chore(deps): upgrade next.js to 16.2.4 (#4460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): upgrade next.js to 16.2.4 - Bump next and @next/env to 16.2.4 across root, apps/sim, apps/docs - Replace next-runtime-env's env() helper (calls unstable_noStore(), rejected by Next 16.2 outside request scope) with a direct window.__ENV / process.env getter - Add export const dynamic = 'force-dynamic' on landing /privacy and /terms pages so NEXT_PUBLIC_* runtime env reads aren't baked at build * fix(whitelabel): force dynamic rendering for manifest.ts Without this, NEXT_PUBLIC_BRAND_* values are baked into the manifest at build time. Pairs with the next-runtime-env removal in the prior commit, restoring Docker runtime injection for whitelabel deployments. * fix(oauth): wrap consent page useSearchParams in Suspense Next 16.2's stricter prerender check fails the build when useSearchParams() is used without a Suspense boundary. Splits the client component into an outer wrapper and inner body. * fix(whitelabel): force dynamic rendering for landing segment Client components in (landing) (e.g. Navbar) read NEXT_PUBLIC_BRAND_* via getEnv. Without this, SSR prerender would bake the build-time process.env values into HTML, mismatching window.__ENV after hydration in Docker runtime-env deployments. Cascades to all landing routes via the layout. * revert(whitelabel): drop force-dynamic from landing layout Cascading force-dynamic neutered dynamicParams = false + generateStaticParams on /blog/[slug], /integrations/[slug], /models/[provider], /models/[provider]/[model] — killing static prerender for SEO-critical pages. The hydration concern only materializes for whitelabel Docker deployments where build-time and runtime NEXT_PUBLIC_BRAND_* differ; those deployments can set the vars at build instead. Keeping force-dynamic on /privacy, /terms, and /manifest where it actually matters. * fix(prerender): wrap useSearchParams callsites for Next 16.2 Next 16.2 fails the build when a client component using useSearchParams() is statically prerendered without a Suspense boundary. - Wrap landing Navbar in Suspense (imported by /oauth/consent and other pages) - Add force-dynamic to reset-password, invite/[id], and unsubscribe pages whose client bodies call useSearchParams * fix(navbar): preserve SSR HTML, drop Suspense bailout Reading useSearchParams() forced a Suspense fallback that emitted no navbar HTML during SSR — leaving crawlers and no-JS users without nav. The 'home' query param only affects client-side link targets, so read it from window.location in an effect after hydration. Restores full SSR navbar markup. * chore: trim verbose comments in next.js upgrade The force-dynamic export name is self-documenting; the remaining env.ts comment is tightened to the essential WHY (why we don't use next-runtime-env's helper). --- apps/docs/package.json | 2 +- apps/sim/app/(auth)/oauth/consent/page.tsx | 10 ++++++- apps/sim/app/(auth)/reset-password/page.tsx | 2 ++ .../(landing)/components/navbar/navbar.tsx | 7 +++-- apps/sim/app/(landing)/privacy/page.tsx | 2 ++ apps/sim/app/(landing)/terms/page.tsx | 2 ++ apps/sim/app/invite/[id]/page.tsx | 2 ++ apps/sim/app/manifest.ts | 2 ++ apps/sim/app/unsubscribe/page.tsx | 2 ++ apps/sim/lib/core/config/env.ts | 14 ++++++---- apps/sim/next.config.ts | 10 +++---- apps/sim/package.json | 6 ++-- bun.lock | 28 +++++++++---------- package.json | 4 +-- 14 files changed, 58 insertions(+), 35 deletions(-) diff --git a/apps/docs/package.json b/apps/docs/package.json index d9d9a53f519..d614fe60b0a 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -26,7 +26,7 @@ "fumadocs-openapi": "10.8.1", "fumadocs-ui": "16.8.5", "lucide-react": "^0.511.0", - "next": "16.1.6", + "next": "16.2.4", "next-themes": "^0.4.6", "postgres": "^3.4.5", "react": "19.2.4", diff --git a/apps/sim/app/(auth)/oauth/consent/page.tsx b/apps/sim/app/(auth)/oauth/consent/page.tsx index 8addf4e82b4..ea624d9b87f 100644 --- a/apps/sim/app/(auth)/oauth/consent/page.tsx +++ b/apps/sim/app/(auth)/oauth/consent/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useState } from 'react' +import { Suspense, useCallback, useEffect, useState } from 'react' import { ArrowLeftRight } from 'lucide-react' import Image from 'next/image' import { useRouter, useSearchParams } from 'next/navigation' @@ -25,6 +25,14 @@ interface ClientInfo { } export default function OAuthConsentPage() { + return ( + + + + ) +} + +function OAuthConsentInner() { const router = useRouter() const searchParams = useSearchParams() const { data: session } = useSession() diff --git a/apps/sim/app/(auth)/reset-password/page.tsx b/apps/sim/app/(auth)/reset-password/page.tsx index cb6470bba0e..8d9ee5dfd42 100644 --- a/apps/sim/app/(auth)/reset-password/page.tsx +++ b/apps/sim/app/(auth)/reset-password/page.tsx @@ -5,4 +5,6 @@ export const metadata: Metadata = { title: 'Reset Password', } +export const dynamic = 'force-dynamic' + export default ResetPasswordPage diff --git a/apps/sim/app/(landing)/components/navbar/navbar.tsx b/apps/sim/app/(landing)/components/navbar/navbar.tsx index 4ee2fdb2aca..e5503dcb27e 100644 --- a/apps/sim/app/(landing)/components/navbar/navbar.tsx +++ b/apps/sim/app/(landing)/components/navbar/navbar.tsx @@ -4,7 +4,6 @@ import { useCallback, useContext, useEffect, useRef, useState, useSyncExternalSt import dynamic from 'next/dynamic' import Image from 'next/image' import Link from 'next/link' -import { useSearchParams } from 'next/navigation' import { GithubOutlineIcon } from '@/components/icons' import { cn } from '@/lib/core/utils/cn' import { SessionContext } from '@/app/_shell/providers/session-provider' @@ -52,12 +51,14 @@ interface NavbarProps { export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps) { const brand = getBrandConfig() - const searchParams = useSearchParams() const sessionCtx = useContext(SessionContext) const session = sessionCtx?.data ?? null const isSessionPending = sessionCtx?.isPending ?? true const isAuthenticated = Boolean(session?.user?.id) - const isBrowsingHome = searchParams.has('home') + const [isBrowsingHome, setIsBrowsingHome] = useState(false) + useEffect(() => { + setIsBrowsingHome(new URLSearchParams(window.location.search).has('home')) + }, []) const useHomeLinks = isAuthenticated || isBrowsingHome const logoHref = useHomeLinks ? '/?home' : '/' const mounted = useSyncExternalStore( diff --git a/apps/sim/app/(landing)/privacy/page.tsx b/apps/sim/app/(landing)/privacy/page.tsx index 44140c62bb6..1081c090b82 100644 --- a/apps/sim/app/(landing)/privacy/page.tsx +++ b/apps/sim/app/(landing)/privacy/page.tsx @@ -3,6 +3,8 @@ import Link from 'next/link' import { getEnv } from '@/lib/core/config/env' import { ExternalRedirect, LegalLayout } from '@/app/(landing)/components' +export const dynamic = 'force-dynamic' + export const metadata: Metadata = { title: 'Privacy Policy', description: diff --git a/apps/sim/app/(landing)/terms/page.tsx b/apps/sim/app/(landing)/terms/page.tsx index 08c16aeaa14..5d49f2bc52a 100644 --- a/apps/sim/app/(landing)/terms/page.tsx +++ b/apps/sim/app/(landing)/terms/page.tsx @@ -3,6 +3,8 @@ import Link from 'next/link' import { getEnv } from '@/lib/core/config/env' import { ExternalRedirect, LegalLayout } from '@/app/(landing)/components' +export const dynamic = 'force-dynamic' + export const metadata: Metadata = { title: 'Terms of Service', description: diff --git a/apps/sim/app/invite/[id]/page.tsx b/apps/sim/app/invite/[id]/page.tsx index e04a2ca7743..ac302790523 100644 --- a/apps/sim/app/invite/[id]/page.tsx +++ b/apps/sim/app/invite/[id]/page.tsx @@ -6,4 +6,6 @@ export const metadata: Metadata = { robots: { index: false }, } +export const dynamic = 'force-dynamic' + export default Invite diff --git a/apps/sim/app/manifest.ts b/apps/sim/app/manifest.ts index d66d2db1a17..cb91437f3c1 100644 --- a/apps/sim/app/manifest.ts +++ b/apps/sim/app/manifest.ts @@ -1,6 +1,8 @@ import type { MetadataRoute } from 'next' import { getBrandConfig } from '@/ee/whitelabeling' +export const dynamic = 'force-dynamic' + export default function manifest(): MetadataRoute.Manifest { const brand = getBrandConfig() diff --git a/apps/sim/app/unsubscribe/page.tsx b/apps/sim/app/unsubscribe/page.tsx index d1b3ec2de10..2258e81c93e 100644 --- a/apps/sim/app/unsubscribe/page.tsx +++ b/apps/sim/app/unsubscribe/page.tsx @@ -6,4 +6,6 @@ export const metadata: Metadata = { robots: { index: false }, } +export const dynamic = 'force-dynamic' + export default Unsubscribe diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index b2c8c2871bd..4d138316597 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -1,14 +1,16 @@ import { createEnv } from '@t3-oss/env-nextjs' -import { env as runtimeEnv } from 'next-runtime-env' import { z } from 'zod' /** - * Universal environment variable getter that works in both client and server contexts. - * - Client-side: Uses next-runtime-env for runtime injection (supports Docker runtime vars) - * - Server-side: Falls back to process.env when runtimeEnv returns undefined - * - Provides seamless Docker runtime variable support for NEXT_PUBLIC_ vars + * Reads NEXT_PUBLIC_* env vars in both client and server contexts. + * Client reads `window.__ENV` (populated by ``); server reads `process.env`. + * We do not use next-runtime-env's `env()` helper because it calls `unstable_noStore()`, + * which Next 16.2+ rejects outside a request scope. */ -const getEnv = (variable: string) => runtimeEnv(variable) ?? process.env[variable] +const getEnv = (variable: string): string | undefined => { + if (typeof window === 'undefined') return process.env[variable] + return window.__ENV?.[variable] ?? process.env[variable] +} // biome-ignore format: keep alignment for readability export const env = createEnv({ diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index e349e2c865b..7a96c063cfe 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -1,5 +1,5 @@ import type { NextConfig } from 'next' -import { env, getEnv, isTruthy } from './lib/core/config/env' +import { env, isTruthy } from './lib/core/config/env' import { isDev } from './lib/core/config/feature-flags' import { getChatEmbedCSPPolicy, @@ -40,13 +40,13 @@ const nextConfig: NextConfig = { hostname: 'lh3.googleusercontent.com', }, // Brand logo domain if configured - ...(getEnv('NEXT_PUBLIC_BRAND_LOGO_URL') + ...(process.env.NEXT_PUBLIC_BRAND_LOGO_URL ? (() => { try { return [ { protocol: 'https' as const, - hostname: new URL(getEnv('NEXT_PUBLIC_BRAND_LOGO_URL')!).hostname, + hostname: new URL(process.env.NEXT_PUBLIC_BRAND_LOGO_URL!).hostname, }, ] } catch { @@ -55,13 +55,13 @@ const nextConfig: NextConfig = { })() : []), // Brand favicon domain if configured - ...(getEnv('NEXT_PUBLIC_BRAND_FAVICON_URL') + ...(process.env.NEXT_PUBLIC_BRAND_FAVICON_URL ? (() => { try { return [ { protocol: 'https' as const, - hostname: new URL(getEnv('NEXT_PUBLIC_BRAND_FAVICON_URL')!).hostname, + hostname: new URL(process.env.NEXT_PUBLIC_BRAND_FAVICON_URL!).hostname, }, ] } catch { diff --git a/apps/sim/package.json b/apps/sim/package.json index 9d0c3e8a932..9eadf53bcd4 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -160,7 +160,7 @@ "mongodb": "6.19.0", "mysql2": "3.14.3", "neo4j-driver": "6.0.1", - "next": "16.1.6", + "next": "16.2.4", "next-mdx-remote": "^5.0.0", "next-runtime-env": "3.3.0", "next-themes": "^0.4.6", @@ -250,8 +250,8 @@ "sharp" ], "overrides": { - "next": "16.1.6", - "@next/env": "16.1.6", + "next": "16.2.4", + "@next/env": "16.2.4", "drizzle-orm": "^0.45.2", "postgres": "^3.4.5", "react-floater": { diff --git a/bun.lock b/bun.lock index 979cb1eb048..ecaf2a67bbf 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,7 @@ "fumadocs-openapi": "10.8.1", "fumadocs-ui": "16.8.5", "lucide-react": "^0.511.0", - "next": "16.1.6", + "next": "16.2.4", "next-themes": "^0.4.6", "postgres": "^3.4.5", "react": "19.2.4", @@ -213,7 +213,7 @@ "mongodb": "6.19.0", "mysql2": "3.14.3", "neo4j-driver": "6.0.1", - "next": "16.1.6", + "next": "16.2.4", "next-mdx-remote": "^5.0.0", "next-runtime-env": "3.3.0", "next-themes": "^0.4.6", @@ -482,9 +482,9 @@ "sharp", ], "overrides": { - "@next/env": "16.1.6", + "@next/env": "16.2.4", "drizzle-orm": "^0.45.2", - "next": "16.1.6", + "next": "16.2.4", "postgres": "^3.4.5", "react": "19.2.4", "react-dom": "19.2.4", @@ -1050,23 +1050,23 @@ "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.100", "", { "os": "win32", "cpu": "x64" }, "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA=="], - "@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="], + "@next/env": ["@next/env@16.2.4", "", {}, "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw=="], "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], @@ -3210,7 +3210,7 @@ "netmask": ["netmask@2.1.1", "", {}, "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA=="], - "next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="], + "next": ["next@16.2.4", "", { "dependencies": { "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.4", "@next/swc-darwin-x64": "16.2.4", "@next/swc-linux-arm64-gnu": "16.2.4", "@next/swc-linux-arm64-musl": "16.2.4", "@next/swc-linux-x64-gnu": "16.2.4", "@next/swc-linux-x64-musl": "16.2.4", "@next/swc-win32-arm64-msvc": "16.2.4", "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q=="], "next-mdx-remote": ["next-mdx-remote@5.0.0", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", "unist-util-remove": "^3.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-RNNbqRpK9/dcIFZs/esQhuLA8jANqlH694yqoDBK8hkVdJUndzzGmnPHa2nyi90N4Z9VmzuSWNRpr5ItT3M7xQ=="], diff --git a/package.json b/package.json index 8e8b78e1802..eea53bc1299 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,8 @@ "overrides": { "react": "19.2.4", "react-dom": "19.2.4", - "next": "16.1.6", - "@next/env": "16.1.6", + "next": "16.2.4", + "@next/env": "16.2.4", "drizzle-orm": "^0.45.2", "postgres": "^3.4.5" }, From 6d4ffff327998065819dc7865446cce3e66e2c17 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 6 May 2026 11:55:11 -0700 Subject: [PATCH 07/17] fix(agiloft): correct response parsing, add EWGetChoiceLineId tool (#4477) * fix(agiloft): correct response parsing, add EWGetChoiceLineId tool * fix(agiloft): address PR review feedback --- apps/docs/content/docs/en/tools/agiloft.mdx | 26 +++- apps/docs/content/docs/en/tools/posthog.mdx | 8 +- .../integrations/data/integrations.json | 8 +- .../sim/app/api/tools/agiloft/attach/route.ts | 8 +- .../app/api/tools/agiloft/retrieve/route.ts | 6 +- apps/sim/blocks/blocks/agiloft.ts | 39 +++++- apps/sim/lib/api/contracts/tools/agiloft.ts | 4 +- apps/sim/tools/agiloft/attachment_info.ts | 10 +- apps/sim/tools/agiloft/create_record.ts | 2 +- apps/sim/tools/agiloft/delete_record.ts | 1 + apps/sim/tools/agiloft/get_choice_line_id.ts | 130 ++++++++++++++++++ apps/sim/tools/agiloft/index.ts | 1 + apps/sim/tools/agiloft/read_record.ts | 1 + apps/sim/tools/agiloft/remove_attachment.ts | 4 +- apps/sim/tools/agiloft/saved_search.ts | 8 +- apps/sim/tools/agiloft/search_records.ts | 59 ++++---- apps/sim/tools/agiloft/select_records.ts | 14 +- apps/sim/tools/agiloft/types.ts | 11 ++ apps/sim/tools/agiloft/update_record.ts | 2 +- apps/sim/tools/agiloft/utils.ts | 10 ++ apps/sim/tools/registry.ts | 2 + 21 files changed, 287 insertions(+), 67 deletions(-) create mode 100644 apps/sim/tools/agiloft/get_choice_line_id.ts diff --git a/apps/docs/content/docs/en/tools/agiloft.mdx b/apps/docs/content/docs/en/tools/agiloft.mdx index 235300ea259..5032b74663d 100644 --- a/apps/docs/content/docs/en/tools/agiloft.mdx +++ b/apps/docs/content/docs/en/tools/agiloft.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" {/* MANUAL-CONTENT-START:intro */} @@ -137,6 +137,28 @@ Delete a record from an Agiloft table. | `id` | string | ID of the deleted record | | `deleted` | boolean | Whether the record was successfully deleted | +### `agiloft_get_choice_line_id` + +Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `instanceUrl` | string | Yes | Agiloft instance URL \(e.g., https://mycompany.agiloft.com\) | +| `knowledgeBase` | string | Yes | Knowledge base name | +| `login` | string | Yes | Agiloft username | +| `password` | string | Yes | Agiloft password | +| `table` | string | Yes | Table name \(e.g., "case", "contracts"\) | +| `fieldName` | string | Yes | Choice field name \(e.g., "priority", "status"\) | +| `value` | string | Yes | Choice display value to resolve \(e.g., "High", "Active"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `choiceLineId` | number | Internal numeric line ID of the choice value | + ### `agiloft_lock_record` Lock, unlock, or check the lock status of an Agiloft record. @@ -254,7 +276,7 @@ List saved searches defined for an Agiloft table. | `searches` | array | List of saved searches for the table | | ↳ `name` | string | Saved search name | | ↳ `label` | string | Saved search display label | -| ↳ `id` | string | Saved search database identifier | +| ↳ `id` | number | Saved search database identifier | | ↳ `description` | string | Saved search description | ### `agiloft_search_records` diff --git a/apps/docs/content/docs/en/tools/posthog.mdx b/apps/docs/content/docs/en/tools/posthog.mdx index 61b50e82810..6b471ef60fb 100644 --- a/apps/docs/content/docs/en/tools/posthog.mdx +++ b/apps/docs/content/docs/en/tools/posthog.mdx @@ -87,7 +87,7 @@ List persons (users) in PostHog. Returns user profiles with their properties and | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `personalApiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) | +| `apiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) | | `region` | string | No | PostHog region: us \(default\) or eu | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `limit` | number | No | Number of persons to return \(default: 100, max: 100\) | @@ -115,7 +115,7 @@ Get detailed information about a specific person in PostHog by their ID or UUID. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `personalApiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) | +| `apiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) | | `region` | string | No | PostHog region: us \(default\) or eu | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `personId` | string | Yes | Person ID or UUID to retrieve \(e.g., "01234567-89ab-cdef-0123-456789abcdef"\) | @@ -139,7 +139,7 @@ Delete a person from PostHog. This will remove all associated events and data. U | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `personalApiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) | +| `apiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) | | `region` | string | No | PostHog region: us \(default\) or eu | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `personId` | string | Yes | Person ID or UUID to delete \(e.g., "01234567-89ab-cdef-0123-456789abcdef"\) | @@ -158,7 +158,7 @@ Execute a HogQL query in PostHog. HogQL is PostHog | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `personalApiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) | +| `apiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) | | `region` | string | No | PostHog region: us \(default\) or eu | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `query` | string | Yes | HogQL query to execute. Example: \{"kind": "HogQLQuery", "query": "SELECT event, count\(\) FROM events WHERE timestamp > now\(\) - INTERVAL 1 DAY GROUP BY event"\} | diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 346cb98feeb..7067930cb24 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -321,7 +321,7 @@ "name": "Agiloft", "description": "Manage records in Agiloft CLM", "longDescription": "Integrate with Agiloft contract lifecycle management to create, read, update, delete, and search records. Supports file attachments, SQL-based selection, saved searches, and record locking across any table in your knowledge base.", - "bgColor": "#FFFFFF", + "bgColor": "#001028", "iconName": "AgiloftIcon", "docsUrl": "https://docs.sim.ai/tools/agiloft", "operations": [ @@ -372,9 +372,13 @@ { "name": "Lock Record", "description": "Lock, unlock, or check the lock status of an Agiloft record." + }, + { + "name": "Get Choice Line ID", + "description": "Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields." } ], - "operationCount": 12, + "operationCount": 13, "triggers": [], "triggerCount": 0, "authType": "none", diff --git a/apps/sim/app/api/tools/agiloft/attach/route.ts b/apps/sim/app/api/tools/agiloft/attach/route.ts index d0ec62e0fd8..edcbdc4c0f3 100644 --- a/apps/sim/app/api/tools/agiloft/attach/route.ts +++ b/apps/sim/app/api/tools/agiloft/attach/route.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { agiloftAttachContract } from '@/lib/api/contracts/tools/agiloft' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' @@ -90,7 +91,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const agiloftResponse = await fetch(url, { method: 'PUT', headers: { - 'Content-Type': userFile.type || 'application/octet-stream', + 'Content-Type': 'application/octet-stream', Authorization: `Bearer ${token}`, }, body: new Uint8Array(fileBuffer), @@ -136,9 +137,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } catch (error) { logger.error(`[${requestId}] Error attaching file to Agiloft:`, error) - return NextResponse.json( - { success: false, error: error instanceof Error ? error.message : 'Internal server error' }, - { status: 500 } - ) + return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) } }) diff --git a/apps/sim/app/api/tools/agiloft/retrieve/route.ts b/apps/sim/app/api/tools/agiloft/retrieve/route.ts index 3f94c8bc739..64bd72daae8 100644 --- a/apps/sim/app/api/tools/agiloft/retrieve/route.ts +++ b/apps/sim/app/api/tools/agiloft/retrieve/route.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { agiloftRetrieveContract } from '@/lib/api/contracts/tools/agiloft' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' @@ -127,9 +128,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } catch (error) { logger.error(`[${requestId}] Error retrieving Agiloft attachment:`, error) - return NextResponse.json( - { success: false, error: error instanceof Error ? error.message : 'Internal server error' }, - { status: 500 } - ) + return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) } }) diff --git a/apps/sim/blocks/blocks/agiloft.ts b/apps/sim/blocks/blocks/agiloft.ts index 36e571dad99..ee1791d3048 100644 --- a/apps/sim/blocks/blocks/agiloft.ts +++ b/apps/sim/blocks/blocks/agiloft.ts @@ -13,7 +13,7 @@ export const AgiloftBlock: BlockConfig = { category: 'tools', integrationType: IntegrationType.Productivity, tags: ['automation'], - bgColor: '#FFFFFF', + bgColor: '#001028', icon: AgiloftIcon, authMode: AuthMode.ApiKey, @@ -35,6 +35,7 @@ export const AgiloftBlock: BlockConfig = { { label: 'Remove Attachment', id: 'remove_attachment' }, { label: 'Attachment Info', id: 'attachment_info' }, { label: 'Lock Record', id: 'lock_record' }, + { label: 'Get Choice Line ID', id: 'get_choice_line_id' }, ], value: () => 'search_records', }, @@ -44,7 +45,6 @@ export const AgiloftBlock: BlockConfig = { type: 'short-input', placeholder: 'https://mycompany.agiloft.com', required: true, - password: false, }, { id: 'knowledgeBase', @@ -151,16 +151,36 @@ export const AgiloftBlock: BlockConfig = { id: 'fieldName', title: 'Field Name', type: 'short-input', - placeholder: 'e.g., attached_docs', + placeholder: 'e.g., attached_docs, priority', condition: { field: 'operation', - value: ['attach_file', 'retrieve_attachment', 'remove_attachment', 'attachment_info'], + value: [ + 'attach_file', + 'retrieve_attachment', + 'remove_attachment', + 'attachment_info', + 'get_choice_line_id', + ], }, required: { field: 'operation', - value: ['attach_file', 'retrieve_attachment', 'remove_attachment', 'attachment_info'], + value: [ + 'attach_file', + 'retrieve_attachment', + 'remove_attachment', + 'attachment_info', + 'get_choice_line_id', + ], }, }, + { + id: 'value', + title: 'Choice Value', + type: 'short-input', + placeholder: 'e.g., High, Active', + condition: { field: 'operation', value: 'get_choice_line_id' }, + required: { field: 'operation', value: 'get_choice_line_id' }, + }, { id: 'uploadFile', title: 'File', @@ -254,6 +274,7 @@ export const AgiloftBlock: BlockConfig = { 'agiloft_attachment_info', 'agiloft_create_record', 'agiloft_delete_record', + 'agiloft_get_choice_line_id', 'agiloft_lock_record', 'agiloft_read_record', 'agiloft_remove_attachment', @@ -288,7 +309,8 @@ export const AgiloftBlock: BlockConfig = { data: { type: 'string', description: 'Record data as JSON' }, query: { type: 'string', description: 'Search query' }, where: { type: 'string', description: 'SQL WHERE clause for select' }, - fieldName: { type: 'string', description: 'Attachment field name' }, + fieldName: { type: 'string', description: 'Attachment field name or choice field name' }, + value: { type: 'string', description: 'Choice value to resolve to its line ID' }, attachFile: { type: 'file', description: 'File to attach' }, fileName: { type: 'string', description: 'Name for the attached file' }, position: { type: 'string', description: 'Attachment position index' }, @@ -403,5 +425,10 @@ export const AgiloftBlock: BlockConfig = { description: 'Minutes until the lock expires', condition: { field: 'operation', value: 'lock_record' }, }, + choiceLineId: { + type: 'number', + description: 'Internal numeric ID of the resolved choice value', + condition: { field: 'operation', value: 'get_choice_line_id' }, + }, }, } diff --git a/apps/sim/lib/api/contracts/tools/agiloft.ts b/apps/sim/lib/api/contracts/tools/agiloft.ts index 38b18657eb9..f8c6e1f565c 100644 --- a/apps/sim/lib/api/contracts/tools/agiloft.ts +++ b/apps/sim/lib/api/contracts/tools/agiloft.ts @@ -50,8 +50,8 @@ export const agiloftAttachBodySchema = z.object({ table: z.string().min(1, 'Table is required'), recordId: z.string().min(1, 'Record ID is required'), fieldName: z.string().min(1, 'Field name is required'), - file: FileInputSchema.optional().nullable(), - fileName: z.string().optional().nullable(), + file: FileInputSchema.optional(), + fileName: z.string().optional(), }) export const agiloftRetrieveContract = defineRouteContract({ diff --git a/apps/sim/tools/agiloft/attachment_info.ts b/apps/sim/tools/agiloft/attachment_info.ts index 38471b74e3b..07986303fe8 100644 --- a/apps/sim/tools/agiloft/attachment_info.ts +++ b/apps/sim/tools/agiloft/attachment_info.ts @@ -91,9 +91,13 @@ export const agiloftAttachmentInfoTool: ToolConfig< for (let i = 0; i < result.length; i++) { const item = result[i] as Record attachments.push({ - position: (item.position as number) ?? i, - name: (item.name as string) ?? (item.filename as string) ?? '', - size: (item.size as number) ?? 0, + position: (item.filePosition as number) ?? (item.position as number) ?? i, + name: + (item.fileName as string) ?? + (item.name as string) ?? + (item.filename as string) ?? + '', + size: (item.size as number) ?? (item.fileSize as number) ?? 0, }) } } diff --git a/apps/sim/tools/agiloft/create_record.ts b/apps/sim/tools/agiloft/create_record.ts index d89943f9750..f4763f55bad 100644 --- a/apps/sim/tools/agiloft/create_record.ts +++ b/apps/sim/tools/agiloft/create_record.ts @@ -72,7 +72,7 @@ export const agiloftCreateRecordTool: ToolConfig ({ url: buildCreateRecordUrl(base, params), method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body, }), async (response) => { diff --git a/apps/sim/tools/agiloft/delete_record.ts b/apps/sim/tools/agiloft/delete_record.ts index 3796459dd64..42538104961 100644 --- a/apps/sim/tools/agiloft/delete_record.ts +++ b/apps/sim/tools/agiloft/delete_record.ts @@ -60,6 +60,7 @@ export const agiloftDeleteRecordTool: ToolConfig ({ url: buildDeleteRecordUrl(base, params), method: 'DELETE', + headers: { Accept: 'application/json' }, }), async (response) => { if (!response.ok) { diff --git a/apps/sim/tools/agiloft/get_choice_line_id.ts b/apps/sim/tools/agiloft/get_choice_line_id.ts new file mode 100644 index 00000000000..11df1040565 --- /dev/null +++ b/apps/sim/tools/agiloft/get_choice_line_id.ts @@ -0,0 +1,130 @@ +import type { + AgiloftGetChoiceLineIdParams, + AgiloftGetChoiceLineIdResponse, +} from '@/tools/agiloft/types' +import { buildGetChoiceLineIdUrl, executeAgiloftRequest } from '@/tools/agiloft/utils' +import type { ToolConfig } from '@/tools/types' + +export const agiloftGetChoiceLineIdTool: ToolConfig< + AgiloftGetChoiceLineIdParams, + AgiloftGetChoiceLineIdResponse +> = { + id: 'agiloft_get_choice_line_id', + name: 'Agiloft Get Choice Line ID', + description: + 'Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields.', + version: '1.0.0', + + params: { + instanceUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)', + }, + knowledgeBase: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Knowledge base name', + }, + login: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Agiloft password', + }, + table: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table name (e.g., "case", "contracts")', + }, + fieldName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Choice field name (e.g., "priority", "status")', + }, + value: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Choice display value to resolve (e.g., "High", "Active")', + }, + }, + + request: { + url: 'https://placeholder.agiloft.com', + method: 'GET', + headers: () => ({}), + }, + + directExecution: async (params) => { + return executeAgiloftRequest( + params, + (base) => ({ + url: buildGetChoiceLineIdUrl(base, params), + method: 'GET', + headers: { Accept: 'application/json' }, + }), + async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + output: { choiceLineId: null }, + error: `Agiloft error: ${response.status} - ${errorText}`, + } + } + + const data = (await response.json()) as Record + const result = data.result ?? data + let choiceLineId: number | null = null + + if (typeof result === 'number') { + choiceLineId = result + } else if (typeof result === 'string') { + const parsed = Number(result) + choiceLineId = Number.isFinite(parsed) ? parsed : null + } else if (typeof result === 'object' && result !== null) { + const obj = result as Record + const idVal = obj.id ?? obj.choiceLineId ?? obj.lineId + if (typeof idVal === 'number') { + choiceLineId = idVal + } else if (typeof idVal === 'string') { + const parsed = Number(idVal) + choiceLineId = Number.isFinite(parsed) ? parsed : null + } + } + + if (choiceLineId === null) { + return { + success: false, + output: { choiceLineId: null }, + error: `No choice line ID found for value "${params.value}" in field "${params.fieldName}"`, + } + } + + return { + success: data.success !== false, + output: { choiceLineId }, + } + } + ) + }, + + outputs: { + choiceLineId: { + type: 'number', + description: 'Internal numeric line ID of the choice value', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/agiloft/index.ts b/apps/sim/tools/agiloft/index.ts index 2ff7ac6f4f0..c47d18f98c2 100644 --- a/apps/sim/tools/agiloft/index.ts +++ b/apps/sim/tools/agiloft/index.ts @@ -2,6 +2,7 @@ export { agiloftAttachFileTool } from '@/tools/agiloft/attach_file' export { agiloftAttachmentInfoTool } from '@/tools/agiloft/attachment_info' export { agiloftCreateRecordTool } from '@/tools/agiloft/create_record' export { agiloftDeleteRecordTool } from '@/tools/agiloft/delete_record' +export { agiloftGetChoiceLineIdTool } from '@/tools/agiloft/get_choice_line_id' export { agiloftLockRecordTool } from '@/tools/agiloft/lock_record' export { agiloftReadRecordTool } from '@/tools/agiloft/read_record' export { agiloftRemoveAttachmentTool } from '@/tools/agiloft/remove_attachment' diff --git a/apps/sim/tools/agiloft/read_record.ts b/apps/sim/tools/agiloft/read_record.ts index c9760a70089..70b015c43bf 100644 --- a/apps/sim/tools/agiloft/read_record.ts +++ b/apps/sim/tools/agiloft/read_record.ts @@ -65,6 +65,7 @@ export const agiloftReadRecordTool: ToolConfig ({ url: buildReadRecordUrl(base, params), method: 'GET', + headers: { Accept: 'application/json' }, }), async (response) => { if (!response.ok) { diff --git a/apps/sim/tools/agiloft/remove_attachment.ts b/apps/sim/tools/agiloft/remove_attachment.ts index 3117017719f..7e9a9d6f2d4 100644 --- a/apps/sim/tools/agiloft/remove_attachment.ts +++ b/apps/sim/tools/agiloft/remove_attachment.ts @@ -67,7 +67,7 @@ export const agiloftRemoveAttachmentTool: ToolConfig< request: { url: 'https://placeholder.agiloft.com', - method: 'GET', + method: 'DELETE', headers: () => ({}), }, @@ -76,7 +76,7 @@ export const agiloftRemoveAttachmentTool: ToolConfig< params, (base) => ({ url: buildRemoveAttachmentUrl(base, params), - method: 'GET', + method: 'DELETE', }), async (response) => { const text = await response.text() diff --git a/apps/sim/tools/agiloft/saved_search.ts b/apps/sim/tools/agiloft/saved_search.ts index 8b28cbd140b..8d645d871d0 100644 --- a/apps/sim/tools/agiloft/saved_search.ts +++ b/apps/sim/tools/agiloft/saved_search.ts @@ -107,8 +107,12 @@ export const agiloftSavedSearchTool: ToolConfig< properties: { name: { type: 'string', description: 'Saved search name' }, label: { type: 'string', description: 'Saved search display label' }, - id: { type: 'string', description: 'Saved search database identifier' }, - description: { type: 'string', description: 'Saved search description' }, + id: { type: 'number', description: 'Saved search database identifier' }, + description: { + type: 'string', + description: 'Saved search description', + optional: true, + }, }, }, }, diff --git a/apps/sim/tools/agiloft/search_records.ts b/apps/sim/tools/agiloft/search_records.ts index 422140a81aa..b05465c0be5 100644 --- a/apps/sim/tools/agiloft/search_records.ts +++ b/apps/sim/tools/agiloft/search_records.ts @@ -94,45 +94,44 @@ export const agiloftSearchRecordsTool: ToolConfig< const data = (await response.json()) as Record const records: Record[] = [] + const result = (data.result ?? data) as Record - if (data.result && Array.isArray(data.result)) { - for (const item of data.result as Record[]) { + if (Array.isArray(result)) { + for (const item of result as Record[]) { records.push(item) } - } else if (Array.isArray(data)) { - for (const item of data as Record[]) { - records.push(item) - } - } else if (data.results && Array.isArray(data.results)) { - for (const item of data.results as Record[]) { - records.push(item) - } - } else if (data.records && Array.isArray(data.records)) { - for (const item of data.records as Record[]) { - records.push(item) - } - } else if (typeof data.EWREST_length === 'number') { - const count = data.EWREST_length as number - for (let i = 0; i < count; i++) { - const record: Record = {} - for (const key of Object.keys(data)) { - const match = key.match(/^EWREST_(.+)_(\d+)$/) - if (match && Number(match[2]) === i) { - record[match[1]] = data[key] + } else { + const lengthRaw = result.EWREST_length ?? data.EWREST_length + const count = typeof lengthRaw === 'string' ? Number(lengthRaw) : (lengthRaw as number) + if (typeof count === 'number' && Number.isFinite(count)) { + const source = (result.EWREST_length != null ? result : data) as Record + for (let i = 0; i < count; i++) { + const record: Record = {} + for (const key of Object.keys(source)) { + const match = key.match(/^EWREST_(.+)_(\d+)$/) + if (match && Number(match[2]) === i) { + record[match[1]] = source[key] + } + } + if (Object.keys(record).length > 0) { + records.push(record) } - } - if (Object.keys(record).length > 0) { - records.push(record) } } } - const totalCount = - (data.totalCount as number) ?? - (data.total as number) ?? - (data.count as number) ?? - (data.EWREST_length as number) ?? + const totalCountRaw = + result.totalCount ?? + result.total ?? + result.count ?? + result.EWREST_length ?? + data.totalCount ?? + data.total ?? + data.count ?? + data.EWREST_length ?? records.length + const totalCount = + typeof totalCountRaw === 'string' ? Number(totalCountRaw) : (totalCountRaw as number) const page = params.page ? Number(params.page) : 0 const limit = params.limit ? Number(params.limit) : 25 diff --git a/apps/sim/tools/agiloft/select_records.ts b/apps/sim/tools/agiloft/select_records.ts index 521ea497fbd..de4be3139cb 100644 --- a/apps/sim/tools/agiloft/select_records.ts +++ b/apps/sim/tools/agiloft/select_records.ts @@ -95,14 +95,22 @@ export const agiloftSelectRecordsTool: ToolConfig< } } - const totalCount = - data.EWREST_id_length ?? data.totalCount ?? data.total ?? data.count ?? recordIds.length + const totalCountRaw = + result.EWREST_id_length ?? + result.totalCount ?? + result.total ?? + result.count ?? + data.EWREST_id_length ?? + data.totalCount ?? + data.total ?? + data.count ?? + recordIds.length return { success: data.success !== false, output: { recordIds, - totalCount: Number(totalCount), + totalCount: Number(totalCountRaw), }, } } diff --git a/apps/sim/tools/agiloft/types.ts b/apps/sim/tools/agiloft/types.ts index 9d132631556..849c6ab05c9 100644 --- a/apps/sim/tools/agiloft/types.ts +++ b/apps/sim/tools/agiloft/types.ts @@ -160,3 +160,14 @@ export interface AgiloftRemoveAttachmentResponse extends ToolResponse { remainingAttachments: number } } + +export interface AgiloftGetChoiceLineIdParams extends AgiloftBaseParams { + fieldName: string + value: string +} + +export interface AgiloftGetChoiceLineIdResponse extends ToolResponse { + output: { + choiceLineId: number | null + } +} diff --git a/apps/sim/tools/agiloft/update_record.ts b/apps/sim/tools/agiloft/update_record.ts index 0c3f8a2d096..661be1b3a8a 100644 --- a/apps/sim/tools/agiloft/update_record.ts +++ b/apps/sim/tools/agiloft/update_record.ts @@ -78,7 +78,7 @@ export const agiloftUpdateRecordTool: ToolConfig ({ url: buildUpdateRecordUrl(base, params), method: 'PUT', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body, }), async (response) => { diff --git a/apps/sim/tools/agiloft/utils.ts b/apps/sim/tools/agiloft/utils.ts index 252dcb4a819..47184deb5fb 100644 --- a/apps/sim/tools/agiloft/utils.ts +++ b/apps/sim/tools/agiloft/utils.ts @@ -4,6 +4,7 @@ import type { AgiloftAttachmentInfoParams, AgiloftBaseParams, AgiloftDeleteRecordParams, + AgiloftGetChoiceLineIdParams, AgiloftLockRecordParams, AgiloftReadRecordParams, AgiloftRemoveAttachmentParams, @@ -243,6 +244,15 @@ export function buildAttachFileUrl( return `${base}/ewws/EWAttach?$KB=${kb}&$table=${table}&$lang=en&id=${recordId}&field=${fieldName}&fileName=${encodedFileName}` } +export function buildGetChoiceLineIdUrl( + base: string, + params: AgiloftGetChoiceLineIdParams +): string { + const field = encodeURIComponent(params.fieldName.trim()) + const value = encodeURIComponent(params.value.trim()) + return `${base}/ewws/EWGetChoiceLineId/.json?${buildEwBaseQuery(params)}&field=${field}&value=${value}` +} + export function getLockHttpMethod(lockAction: string): HttpMethod { switch (lockAction) { case 'lock': diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 9130ac52dee..6bc8feea7e3 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -60,6 +60,7 @@ import { agiloftAttachmentInfoTool, agiloftCreateRecordTool, agiloftDeleteRecordTool, + agiloftGetChoiceLineIdTool, agiloftLockRecordTool, agiloftReadRecordTool, agiloftRemoveAttachmentTool, @@ -3003,6 +3004,7 @@ export const tools: Record = { agiloft_attachment_info: agiloftAttachmentInfoTool, agiloft_create_record: agiloftCreateRecordTool, agiloft_delete_record: agiloftDeleteRecordTool, + agiloft_get_choice_line_id: agiloftGetChoiceLineIdTool, agiloft_lock_record: agiloftLockRecordTool, agiloft_read_record: agiloftReadRecordTool, agiloft_remove_attachment: agiloftRemoveAttachmentTool, From bfd0f46119aadfe472df85a508bce6efb2b92a57 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 6 May 2026 12:30:57 -0700 Subject: [PATCH 08/17] improvement(next): bundle and CI cache config (#4478) - drop redundant turbopack config (Next 16 defaults) - remove lucide-react/date-fns from optimizePackageImports (built-in defaults) - enable turbopackFileSystemCacheForBuild for warm CI builds - disable poweredByHeader - swap actions/cache for Blacksmith sticky disk on .next/cache --- .github/workflows/test-build.yml | 8 +++----- apps/sim/next.config.ts | 9 ++------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 68c5a9901de..4359fd261ea 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -44,13 +44,11 @@ jobs: key: ${{ github.repository }}-turbo-cache path: ./.turbo - - name: Restore Next.js build cache - uses: actions/cache@v5 + - name: Mount Next.js build cache (Sticky Disk) + uses: useblacksmith/stickydisk@v1 with: + key: ${{ github.repository }}-nextjs-cache path: ./apps/sim/.next/cache - key: ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }} - restore-keys: | - ${{ runner.os }}-nextjs- - name: Install dependencies run: bun install --frozen-lockfile diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index 7a96c063cfe..68ad3b37fa7 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -10,6 +10,7 @@ import { const nextConfig: NextConfig = { devIndicators: false, + poweredByHeader: false, images: { formats: ['image/avif', 'image/webp'], remotePatterns: [ @@ -75,9 +76,6 @@ const nextConfig: NextConfig = { ignoreBuildErrors: isTruthy(env.DOCKER_BUILD), }, output: isTruthy(env.DOCKER_BUILD) ? 'standalone' : undefined, - turbopack: { - resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'], - }, serverExternalPackages: [ '@1password/sdk', 'unpdf', @@ -99,11 +97,9 @@ const nextConfig: NextConfig = { }, experimental: { optimizeCss: true, - turbopackSourceMaps: false, - turbopackFileSystemCacheForDev: true, preloadEntriesOnStart: false, + turbopackFileSystemCacheForBuild: true, optimizePackageImports: [ - 'lucide-react', 'lodash', 'framer-motion', 'reactflow', @@ -119,7 +115,6 @@ const nextConfig: NextConfig = { '@radix-ui/react-slider', 'streamdown', 'zod', - 'date-fns', ], }, ...(isDev && { From 79ffccc14032267efc085ad6cbb0c755eab44097 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 6 May 2026 12:44:17 -0700 Subject: [PATCH 09/17] feat(emailbison): block, tools, sharepoint v2 block with cleaner code (#4470) * feat(emailbison): block, tools * type improvments * typecheck issue * add email bison trigger, cleanup sharepoint block * address comments * fix tests * error on partial upload failures --- apps/docs/components/icons.tsx | 11 + apps/docs/components/ui/icon-mapping.ts | 3 + .../docs/content/docs/en/tools/emailbison.mdx | 449 +++++++ apps/docs/content/docs/en/tools/meta.json | 1 + .../docs/content/docs/en/tools/sharepoint.mdx | 28 +- .../content/docs/en/triggers/emailbison.mdx | 1178 +++++++++++++++++ apps/docs/content/docs/en/triggers/meta.json | 1 + .../integrations/(shell)/[slug]/page.tsx | 11 +- .../integrations/data/icon-mapping.ts | 4 +- .../integrations/data/integrations.json | 79 +- .../app/api/tools/sharepoint/upload/route.ts | 113 +- apps/sim/blocks/blocks/emailbison.ts | 627 +++++++++ apps/sim/blocks/blocks/sharepoint.ts | 483 +++++++ apps/sim/blocks/registry.ts | 5 +- apps/sim/components/icons.tsx | 11 + apps/sim/lib/webhooks/providers/emailbison.ts | 300 +++++ apps/sim/lib/webhooks/providers/registry.ts | 2 + .../emailbison/attach_leads_to_campaign.ts | 70 + .../tools/emailbison/attach_tags_to_leads.ts | 67 + apps/sim/tools/emailbison/create_campaign.ts | 58 + apps/sim/tools/emailbison/create_lead.ts | 94 ++ apps/sim/tools/emailbison/create_tag.ts | 42 + apps/sim/tools/emailbison/get_lead.ts | 45 + apps/sim/tools/emailbison/index.ts | 14 + apps/sim/tools/emailbison/list_campaigns.ts | 42 + apps/sim/tools/emailbison/list_leads.ts | 85 ++ apps/sim/tools/emailbison/list_replies.ts | 108 ++ apps/sim/tools/emailbison/list_tags.ts | 38 + apps/sim/tools/emailbison/types.ts | 254 ++++ apps/sim/tools/emailbison/update_campaign.ts | 114 ++ .../emailbison/update_campaign_status.ts | 64 + apps/sim/tools/emailbison/update_lead.ts | 106 ++ apps/sim/tools/emailbison/utils.ts | 482 +++++++ apps/sim/tools/registry.ts | 36 +- apps/sim/tools/sharepoint/add_list_items.ts | 17 +- apps/sim/tools/sharepoint/create_list.ts | 46 +- apps/sim/tools/sharepoint/create_page.ts | 20 +- apps/sim/tools/sharepoint/get_list.ts | 92 +- apps/sim/tools/sharepoint/index.ts | 2 + apps/sim/tools/sharepoint/list_sites.ts | 44 +- apps/sim/tools/sharepoint/read_page.ts | 92 +- apps/sim/tools/sharepoint/types.ts | 66 +- apps/sim/tools/sharepoint/update_list.ts | 19 +- apps/sim/tools/sharepoint/upload_file.ts | 60 +- apps/sim/tools/sharepoint/utils.ts | 24 + .../emailbison/email_account_added.ts | 26 + .../emailbison/email_account_disconnected.ts | 26 + .../emailbison/email_account_reconnected.ts | 26 + .../emailbison/email_account_removed.ts | 26 + apps/sim/triggers/emailbison/email_bounced.ts | 26 + apps/sim/triggers/emailbison/email_opened.ts | 26 + apps/sim/triggers/emailbison/email_sent.ts | 27 + apps/sim/triggers/emailbison/index.ts | 17 + .../emailbison/lead_first_contacted.ts | 26 + .../triggers/emailbison/lead_interested.ts | 26 + apps/sim/triggers/emailbison/lead_replied.ts | 26 + .../triggers/emailbison/lead_unsubscribed.ts | 26 + .../triggers/emailbison/manual_email_sent.ts | 26 + apps/sim/triggers/emailbison/tag_attached.ts | 26 + apps/sim/triggers/emailbison/tag_removed.ts | 26 + .../emailbison/untracked_reply_received.ts | 26 + apps/sim/triggers/emailbison/utils.ts | 510 +++++++ .../warmup_disabled_causing_bounces.ts | 26 + .../warmup_disabled_receiving_bounces.ts | 26 + apps/sim/triggers/registry.ts | 36 + 65 files changed, 6263 insertions(+), 250 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/emailbison.mdx create mode 100644 apps/docs/content/docs/en/triggers/emailbison.mdx create mode 100644 apps/sim/blocks/blocks/emailbison.ts create mode 100644 apps/sim/lib/webhooks/providers/emailbison.ts create mode 100644 apps/sim/tools/emailbison/attach_leads_to_campaign.ts create mode 100644 apps/sim/tools/emailbison/attach_tags_to_leads.ts create mode 100644 apps/sim/tools/emailbison/create_campaign.ts create mode 100644 apps/sim/tools/emailbison/create_lead.ts create mode 100644 apps/sim/tools/emailbison/create_tag.ts create mode 100644 apps/sim/tools/emailbison/get_lead.ts create mode 100644 apps/sim/tools/emailbison/index.ts create mode 100644 apps/sim/tools/emailbison/list_campaigns.ts create mode 100644 apps/sim/tools/emailbison/list_leads.ts create mode 100644 apps/sim/tools/emailbison/list_replies.ts create mode 100644 apps/sim/tools/emailbison/list_tags.ts create mode 100644 apps/sim/tools/emailbison/types.ts create mode 100644 apps/sim/tools/emailbison/update_campaign.ts create mode 100644 apps/sim/tools/emailbison/update_campaign_status.ts create mode 100644 apps/sim/tools/emailbison/update_lead.ts create mode 100644 apps/sim/tools/emailbison/utils.ts create mode 100644 apps/sim/triggers/emailbison/email_account_added.ts create mode 100644 apps/sim/triggers/emailbison/email_account_disconnected.ts create mode 100644 apps/sim/triggers/emailbison/email_account_reconnected.ts create mode 100644 apps/sim/triggers/emailbison/email_account_removed.ts create mode 100644 apps/sim/triggers/emailbison/email_bounced.ts create mode 100644 apps/sim/triggers/emailbison/email_opened.ts create mode 100644 apps/sim/triggers/emailbison/email_sent.ts create mode 100644 apps/sim/triggers/emailbison/index.ts create mode 100644 apps/sim/triggers/emailbison/lead_first_contacted.ts create mode 100644 apps/sim/triggers/emailbison/lead_interested.ts create mode 100644 apps/sim/triggers/emailbison/lead_replied.ts create mode 100644 apps/sim/triggers/emailbison/lead_unsubscribed.ts create mode 100644 apps/sim/triggers/emailbison/manual_email_sent.ts create mode 100644 apps/sim/triggers/emailbison/tag_attached.ts create mode 100644 apps/sim/triggers/emailbison/tag_removed.ts create mode 100644 apps/sim/triggers/emailbison/untracked_reply_received.ts create mode 100644 apps/sim/triggers/emailbison/utils.ts create mode 100644 apps/sim/triggers/emailbison/warmup_disabled_causing_bounces.ts create mode 100644 apps/sim/triggers/emailbison/warmup_disabled_receiving_bounces.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index dae53828ccb..c4bc260742b 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -415,6 +415,17 @@ export function MailIcon(props: SVGProps) { ) } +export function EmailBisonIcon(props: SVGProps) { + return ( + + + + ) +} + export function MailServerIcon(props: SVGProps) { return ( = { dynamodb: DynamoDBIcon, elasticsearch: ElasticsearchIcon, elevenlabs: ElevenLabsIcon, + emailbison: EmailBisonIcon, enrich: EnrichSoIcon, evernote: EvernoteIcon, exa: ExaAIIcon, @@ -380,6 +382,7 @@ export const blockTypeToIconMap: Record = { ses: SESIcon, sftp: SftpIcon, sharepoint: MicrosoftSharepointIcon, + sharepoint_v2: MicrosoftSharepointIcon, shopify: ShopifyIcon, similarweb: SimilarwebIcon, sixtyfour: SixtyfourIcon, diff --git a/apps/docs/content/docs/en/tools/emailbison.mdx b/apps/docs/content/docs/en/tools/emailbison.mdx new file mode 100644 index 00000000000..5e3b5d66369 --- /dev/null +++ b/apps/docs/content/docs/en/tools/emailbison.mdx @@ -0,0 +1,449 @@ +--- +title: Email Bison +description: Manage Email Bison leads, campaigns, replies, and tags +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate Email Bison into workflows. Create and update leads, manage campaigns, attach leads to campaigns, list replies, and organize leads with tags. + + + +## Tools + +### `emailbison_list_leads` + +Retrieves leads from Email Bison with optional search and tag filters. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `search` | string | No | Search term for filtering leads | +| `campaignStatus` | string | No | Lead campaign status filter: in_sequence, sequence_finished, sequence_stopped, never_contacted, or replied | +| `tagIds` | array | No | Tag IDs to include | +| `items` | number | No | No description | +| `excludedTagIds` | array | No | Tag IDs to exclude | +| `items` | number | No | No description | +| `withoutTags` | boolean | No | Only return leads without tags | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + +### `emailbison_get_lead` + +Retrieves a lead by Email Bison lead ID or email address. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `leadId` | string | Yes | Lead ID or email address | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + +### `emailbison_create_lead` + +Creates a single lead in Email Bison. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `firstName` | string | Yes | Lead first name | +| `lastName` | string | Yes | Lead last name | +| `email` | string | Yes | Lead email address | +| `title` | string | No | Lead job title | +| `company` | string | No | Lead company | +| `notes` | string | No | Additional notes about the lead | +| `customVariables` | array | No | Custom variables to store on the lead | +| `items` | object | No | Custom variable name | +| `properties` | string | No | Custom variable name | +| `name` | string | No | No description | +| `value` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + +### `emailbison_update_lead` + +Updates an existing Email Bison lead. Fields omitted from a PUT update may be cleared by Email Bison. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `leadId` | string | Yes | Lead ID or email address | +| `firstName` | string | Yes | Lead first name | +| `lastName` | string | Yes | Lead last name | +| `email` | string | Yes | Lead email address | +| `title` | string | No | Lead job title | +| `company` | string | No | Lead company | +| `notes` | string | No | Additional notes about the lead | +| `customVariables` | array | No | Custom variables to store on the lead | +| `items` | object | No | Custom variable name | +| `properties` | string | No | Custom variable name | +| `name` | string | No | No description | +| `value` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + +### `emailbison_list_campaigns` + +Retrieves Email Bison campaigns. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + +### `emailbison_create_campaign` + +Creates a new Email Bison campaign. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | Yes | Campaign name | +| `campaignType` | string | No | Campaign type: outbound or reply_followup | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + +### `emailbison_update_campaign` + +Updates Email Bison campaign settings. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `campaignId` | number | Yes | Campaign ID | +| `name` | string | No | Campaign name | +| `maxEmailsPerDay` | number | No | Maximum emails per day | +| `maxNewLeadsPerDay` | number | No | Maximum new leads per day | +| `plainText` | boolean | No | Send plain text emails | +| `openTracking` | boolean | No | Enable open tracking | +| `reputationBuilding` | boolean | No | Enable reputation building | +| `canUnsubscribe` | boolean | No | Enable unsubscribe link | +| `includeAutoRepliesInStats` | boolean | No | Include auto replies in campaign stats | +| `sequencePrioritization` | string | No | Sequence prioritization: followups or new_leads | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + +### `emailbison_update_campaign_status` + +Pauses, resumes, or archives an Email Bison campaign. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `campaignId` | number | Yes | Campaign ID | +| `action` | string | Yes | Status action: pause, resume, or archive | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + +### `emailbison_attach_leads_to_campaign` + +Adds existing Email Bison leads to a campaign. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `campaignId` | number | Yes | Campaign ID | +| `leadIds` | array | Yes | Lead IDs to add to the campaign | +| `items` | number | No | No description | +| `allowParallelSending` | boolean | No | Force add leads already in sequence in other campaigns | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + +### `emailbison_list_replies` + +Retrieves Email Bison replies with optional status, folder, campaign, sender, lead, and tag filters. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `search` | string | No | Search term for replies | +| `status` | string | No | Reply status: interested, automated_reply, or not_automated_reply | +| `folder` | string | No | Reply folder: inbox, sent, spam, bounced, or all | +| `read` | boolean | No | Filter by read state | +| `campaignId` | number | No | Campaign ID | +| `senderEmailId` | number | No | Sender email ID | +| `leadId` | number | No | Lead ID | +| `tagIds` | array | No | Tag IDs to filter replies by | +| `items` | number | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + +### `emailbison_list_tags` + +Retrieves all Email Bison tags for the authenticated workspace. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + +### `emailbison_create_tag` + +Creates a new Email Bison tag. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | Yes | Tag name | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + +### `emailbison_attach_tags_to_leads` + +Attaches Email Bison tags to one or more leads. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tagIds` | array | Yes | Tag IDs to attach | +| `items` | number | No | No description | +| `leadIds` | array | Yes | Lead IDs to tag | +| `items` | number | No | No description | +| `skipWebhooks` | boolean | No | Skip Email Bison webhooks for this action | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads | +| `campaigns` | array | List of campaigns | +| `replies` | array | List of replies | +| `tags` | array | List of tags | +| `count` | number | Number of returned records | +| `id` | number | Record ID | +| `uuid` | string | Record UUID | +| `name` | string | Campaign or tag name | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `email` | string | Lead email address | +| `status` | string | Record status | +| `success` | boolean | Whether the action succeeded | +| `message` | string | Action message | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 1f780cff3d2..6beab98ac26 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -46,6 +46,7 @@ "dynamodb", "elasticsearch", "elevenlabs", + "emailbison", "enrich", "evernote", "exa", diff --git a/apps/docs/content/docs/en/tools/sharepoint.mdx b/apps/docs/content/docs/en/tools/sharepoint.mdx index 13027ea8261..cf83987bd40 100644 --- a/apps/docs/content/docs/en/tools/sharepoint.mdx +++ b/apps/docs/content/docs/en/tools/sharepoint.mdx @@ -1,12 +1,12 @@ --- -title: Sharepoint +title: SharePoint description: Work with pages and lists --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -73,6 +73,7 @@ Read a specific page from a SharePoint site | `pageId` | string | No | The ID of the page to read. Example: a GUID like 12345678-1234-1234-1234-123456789012 | | `pageName` | string | No | The name of the page to read \(alternative to pageId\). Example: Home.aspx or About-Us.aspx | | `maxPages` | number | No | Maximum number of pages to return when listing all pages \(default: 10, max: 50\) | +| `nextPageUrl` | string | No | Full @odata.nextLink URL from a previous Microsoft Graph page response | #### Output @@ -84,6 +85,7 @@ Read a specific page from a SharePoint site | ↳ `title` | string | The title of the page | | ↳ `webUrl` | string | The URL to access the page | | ↳ `pageLayout` | string | The layout type of the page | +| ↳ `description` | string | The description of the page | | ↳ `createdDateTime` | string | When the page was created | | ↳ `lastModifiedDateTime` | string | When the page was last modified | | `pages` | array | List of SharePoint pages | @@ -93,6 +95,7 @@ Read a specific page from a SharePoint site | ↳ `title` | string | The title of the page | | ↳ `webUrl` | string | The URL to access the page | | ↳ `pageLayout` | string | The layout type of the page | +| ↳ `description` | string | The description of the page | | ↳ `createdDateTime` | string | When the page was created | | ↳ `lastModifiedDateTime` | string | When the page was last modified | | ↳ `content` | object | content output from the tool | @@ -102,6 +105,7 @@ Read a specific page from a SharePoint site | ↳ `content` | string | Extracted text content from the page | | ↳ `canvasLayout` | object | Raw SharePoint canvas layout structure | | `totalPages` | number | Total number of pages found | +| `nextPageUrl` | string | Full Microsoft Graph @odata.nextLink URL for the next page of results | ### `sharepoint_list_sites` @@ -112,7 +116,9 @@ List details of all SharePoint sites | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `siteSelector` | string | No | Select the SharePoint site | +| `siteId` | string | No | The ID of the SharePoint site \(internal use\) | | `groupId` | string | No | The group ID for accessing a group team site. Example: a GUID like 12345678-1234-1234-1234-123456789012 | +| `nextPageUrl` | string | No | Full @odata.nextLink URL from a previous Microsoft Graph page response | #### Output @@ -139,6 +145,7 @@ List details of all SharePoint sites | ↳ `description` | string | The description of the site | | ↳ `createdDateTime` | string | When the site was created | | ↳ `lastModifiedDateTime` | string | When the site was last modified | +| `nextPageUrl` | string | Full Microsoft Graph @odata.nextLink URL for the next page of results | ### `sharepoint_create_list` @@ -153,7 +160,7 @@ Create a new list in a SharePoint site | `listDisplayName` | string | Yes | Display name of the list to create. Example: Project Tasks or Customer Contacts | | `listDescription` | string | No | Description of the list | | `listTemplate` | string | No | List template name \(e.g., 'genericList'\) | -| `pageContent` | string | No | Optional JSON of columns. Either a top-level array of column definitions or an object with \{ columns: \[...\] \}. | +| `pageContent` | json | No | Optional JSON of columns. Either a top-level array of column definitions or an object with \{ columns: \[...\] \}. | #### Output @@ -179,6 +186,9 @@ Get metadata (and optionally columns/items) for a SharePoint list | `siteSelector` | string | No | Select the SharePoint site | | `siteId` | string | No | The ID of the SharePoint site \(internal use\) | | `listId` | string | No | The ID of the list to retrieve. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012 | +| `includeColumns` | boolean | No | Whether to include column definitions when retrieving a specific list | +| `includeItems` | boolean | No | Whether to include list items when retrieving a specific list | +| `nextPageUrl` | string | No | Full @odata.nextLink URL from a previous Microsoft Graph page response | #### Output @@ -194,6 +204,10 @@ Get metadata (and optionally columns/items) for a SharePoint list | ↳ `list` | object | List properties \(e.g., template\) | | ↳ `columns` | array | List column definitions | | `lists` | array | All lists in the site when no listId/title provided | +| `items` | array | List items with expanded fields when reading list items | +| ↳ `id` | string | Item ID | +| ↳ `fields` | object | Field values for the item | +| `nextPageUrl` | string | Full Microsoft Graph @odata.nextLink URL for the next page of results | ### `sharepoint_update_list` @@ -205,9 +219,9 @@ Update the properties (fields) on a SharePoint list item | --------- | ---- | -------- | ----------- | | `siteSelector` | string | No | Select the SharePoint site | | `siteId` | string | No | The ID of the SharePoint site \(internal use\) | -| `listId` | string | No | The ID of the list containing the item. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012 | +| `listId` | string | Yes | The ID of the list containing the item. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012 | | `itemId` | string | Yes | The ID of the list item to update. Example: 1, 42, or 123 | -| `listItemFields` | object | Yes | Field values to update on the list item | +| `listItemFields` | json | Yes | Field values to update on the list item | #### Output @@ -228,7 +242,7 @@ Add a new item to a SharePoint list | `siteSelector` | string | No | Select the SharePoint site | | `siteId` | string | No | The ID of the SharePoint site \(internal use\) | | `listId` | string | Yes | The ID of the list to add the item to. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012 | -| `listItemFields` | object | Yes | Field values for the new list item | +| `listItemFields` | json | Yes | Field values for the new list item | #### Output @@ -250,7 +264,7 @@ Upload files to a SharePoint document library | `driveId` | string | No | The ID of the document library \(drive\). If not provided, uses default drive. Example: b!abc123def456 | | `folderPath` | string | No | Optional folder path within the document library. Example: /Documents/Subfolder or /Shared Documents/Reports | | `fileName` | string | No | Optional: override the uploaded file name. Example: report-2024.pdf | -| `files` | file[] | No | Files to upload to SharePoint | +| `files` | file[] | Yes | Files to upload to SharePoint | #### Output diff --git a/apps/docs/content/docs/en/triggers/emailbison.mdx b/apps/docs/content/docs/en/triggers/emailbison.mdx new file mode 100644 index 00000000000..94a8c041659 --- /dev/null +++ b/apps/docs/content/docs/en/triggers/emailbison.mdx @@ -0,0 +1,1178 @@ +--- +title: Emailbison +description: Available Emailbison triggers for automating workflows +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +Emailbison provides 17 triggers for automating workflows based on events. + +## Triggers + +### Email Bison Contact First Emailed + +Trigger when a contact receives their first campaign email in Email Bison + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `scheduledEmail` | object | scheduledEmail output from the tool | +| ↳ `id` | number | Scheduled email ID | +| ↳ `lead_id` | number | Lead ID | +| ↳ `sequence_step_id` | number | Sequence step ID | +| ↳ `sequence_step_order` | number | Sequence step order | +| ↳ `sequence_step_variant` | number | Sequence step variant | +| ↳ `email_subject` | string | Email subject | +| ↳ `email_body` | string | Email body HTML | +| ↳ `status` | string | Scheduled email status | +| ↳ `scheduled_date_est` | string | Scheduled date in EST | +| ↳ `scheduled_date_local` | string | Scheduled date in local timezone | +| ↳ `local_timezone` | string | Scheduled email local timezone | +| ↳ `sent_at` | string | Email sent timestamp | +| ↳ `opens` | number | Open count | +| ↳ `replies` | number | Reply count | +| ↳ `unique_opens` | number | Unique open count | +| ↳ `unique_replies` | number | Unique reply count | +| ↳ `interested` | string | Interested status | +| ↳ `raw_message_id` | string | Raw email message ID | +| `campaignEvent` | object | campaignEvent output from the tool | +| ↳ `id` | number | Campaign event ID | +| ↳ `event_type` | string | Campaign event type | +| ↳ `created_at_local` | string | Campaign event local creation timestamp | +| ↳ `local_timezone` | string | Campaign event local timezone | +| ↳ `created_at` | string | Campaign event creation timestamp | +| `lead` | object | lead output from the tool | +| ↳ `id` | number | Lead ID | +| ↳ `email` | string | Lead email address | +| ↳ `first_name` | string | Lead first name | +| ↳ `last_name` | string | Lead last name | +| ↳ `status` | string | Lead status | +| ↳ `title` | string | Lead title | +| ↳ `company` | string | Lead company | +| ↳ `custom_variables` | json | Lead custom variables | +| ↳ `emails_sent` | number | Lead emails sent count | +| ↳ `opens` | number | Lead open count | +| ↳ `unique_opens` | number | Lead unique open count | +| ↳ `replies` | number | Lead reply count | +| ↳ `unique_replies` | number | Lead unique reply count | +| ↳ `bounces` | number | Lead bounce count | +| `campaign` | object | campaign output from the tool | +| ↳ `id` | number | Campaign ID | +| ↳ `name` | string | Campaign name | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Contact Interested + +Trigger when a reply is marked interested in Email Bison + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `reply` | object | reply output from the tool | +| ↳ `id` | number | Reply ID | +| ↳ `uuid` | string | Reply UUID | +| ↳ `email_subject` | string | Reply email subject | +| ↳ `interested` | boolean | Whether the reply is marked interested | +| ↳ `automated_reply` | boolean | Whether the reply is automated | +| ↳ `html_body` | string | Reply HTML body | +| ↳ `text_body` | string | Reply plain text body | +| ↳ `raw_body` | string | Raw MIME reply body | +| ↳ `headers` | string | Encoded raw email headers | +| ↳ `date_received` | string | Reply received timestamp | +| ↳ `from_name` | string | Reply sender name | +| ↳ `from_email_address` | string | Reply sender email address | +| ↳ `primary_to_email_address` | string | Primary recipient email address | +| ↳ `to` | json | Reply To recipients | +| ↳ `cc` | json | Reply CC recipients | +| ↳ `bcc` | json | Reply BCC recipients | +| ↳ `parent_id` | number | Parent reply ID | +| ↳ `reply_type` | string | Reply type | +| ↳ `folder` | string | Reply folder | +| ↳ `raw_message_id` | string | Raw email message ID | +| ↳ `created_at` | string | Reply creation timestamp | +| ↳ `updated_at` | string | Reply update timestamp | +| ↳ `attachments` | json | Reply attachments | +| `campaignEvent` | object | campaignEvent output from the tool | +| ↳ `id` | number | Campaign event ID | +| ↳ `event_type` | string | Campaign event type | +| ↳ `created_at_local` | string | Campaign event local creation timestamp | +| ↳ `local_timezone` | string | Campaign event local timezone | +| ↳ `created_at` | string | Campaign event creation timestamp | +| `lead` | object | lead output from the tool | +| ↳ `id` | number | Lead ID | +| ↳ `email` | string | Lead email address | +| ↳ `first_name` | string | Lead first name | +| ↳ `last_name` | string | Lead last name | +| ↳ `status` | string | Lead status | +| ↳ `title` | string | Lead title | +| ↳ `company` | string | Lead company | +| ↳ `custom_variables` | json | Lead custom variables | +| ↳ `emails_sent` | number | Lead emails sent count | +| ↳ `opens` | number | Lead open count | +| ↳ `unique_opens` | number | Lead unique open count | +| ↳ `replies` | number | Lead reply count | +| ↳ `unique_replies` | number | Lead unique reply count | +| ↳ `bounces` | number | Lead bounce count | +| `campaign` | object | campaign output from the tool | +| ↳ `id` | number | Campaign ID | +| ↳ `name` | string | Campaign name | +| `scheduledEmail` | object | scheduledEmail output from the tool | +| ↳ `id` | number | Scheduled email ID | +| ↳ `sequence_step_id` | number | Sequence step ID | +| ↳ `sequence_step_order` | number | Sequence step order | +| ↳ `sequence_step_variant` | number | Sequence step variant | +| ↳ `status` | string | Scheduled email status | +| ↳ `scheduled_date_est` | string | Scheduled date in EST | +| ↳ `scheduled_date_local` | string | Scheduled date in local timezone | +| ↳ `local_timezone` | string | Scheduled email local timezone | +| ↳ `sent_at` | string | Email sent timestamp | +| ↳ `opens` | number | Open count | +| ↳ `replies` | number | Reply count | +| ↳ `unique_opens` | number | Unique open count | +| ↳ `unique_replies` | number | Unique reply count | +| ↳ `interested` | string | Interested status | +| ↳ `raw_message_id` | string | Raw email message ID | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Contact Replied + +Trigger when a campaign lead replies in Email Bison + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `reply` | object | reply output from the tool | +| ↳ `id` | number | Reply ID | +| ↳ `uuid` | string | Reply UUID | +| ↳ `email_subject` | string | Reply email subject | +| ↳ `interested` | boolean | Whether the reply is marked interested | +| ↳ `automated_reply` | boolean | Whether the reply is automated | +| ↳ `html_body` | string | Reply HTML body | +| ↳ `text_body` | string | Reply plain text body | +| ↳ `raw_body` | string | Raw MIME reply body | +| ↳ `headers` | string | Encoded raw email headers | +| ↳ `date_received` | string | Reply received timestamp | +| ↳ `from_name` | string | Reply sender name | +| ↳ `from_email_address` | string | Reply sender email address | +| ↳ `primary_to_email_address` | string | Primary recipient email address | +| ↳ `to` | json | Reply To recipients | +| ↳ `cc` | json | Reply CC recipients | +| ↳ `bcc` | json | Reply BCC recipients | +| ↳ `parent_id` | number | Parent reply ID | +| ↳ `reply_type` | string | Reply type | +| ↳ `folder` | string | Reply folder | +| ↳ `raw_message_id` | string | Raw email message ID | +| ↳ `created_at` | string | Reply creation timestamp | +| ↳ `updated_at` | string | Reply update timestamp | +| ↳ `attachments` | json | Reply attachments | +| `campaignEvent` | object | campaignEvent output from the tool | +| ↳ `id` | number | Campaign event ID | +| ↳ `event_type` | string | Campaign event type | +| ↳ `created_at_local` | string | Campaign event local creation timestamp | +| ↳ `local_timezone` | string | Campaign event local timezone | +| ↳ `created_at` | string | Campaign event creation timestamp | +| `lead` | object | lead output from the tool | +| ↳ `id` | number | Lead ID | +| ↳ `email` | string | Lead email address | +| ↳ `first_name` | string | Lead first name | +| ↳ `last_name` | string | Lead last name | +| ↳ `status` | string | Lead status | +| ↳ `title` | string | Lead title | +| ↳ `company` | string | Lead company | +| ↳ `custom_variables` | json | Lead custom variables | +| ↳ `emails_sent` | number | Lead emails sent count | +| ↳ `opens` | number | Lead open count | +| ↳ `unique_opens` | number | Lead unique open count | +| ↳ `replies` | number | Lead reply count | +| ↳ `unique_replies` | number | Lead unique reply count | +| ↳ `bounces` | number | Lead bounce count | +| `campaign` | object | campaign output from the tool | +| ↳ `id` | number | Campaign ID | +| ↳ `name` | string | Campaign name | +| `scheduledEmail` | object | scheduledEmail output from the tool | +| ↳ `id` | number | Scheduled email ID | +| ↳ `sequence_step_id` | number | Sequence step ID | +| ↳ `sequence_step_order` | number | Sequence step order | +| ↳ `sequence_step_variant` | number | Sequence step variant | +| ↳ `status` | string | Scheduled email status | +| ↳ `scheduled_date_est` | string | Scheduled date in EST | +| ↳ `scheduled_date_local` | string | Scheduled date in local timezone | +| ↳ `local_timezone` | string | Scheduled email local timezone | +| ↳ `sent_at` | string | Email sent timestamp | +| ↳ `opens` | number | Open count | +| ↳ `replies` | number | Reply count | +| ↳ `unique_opens` | number | Unique open count | +| ↳ `unique_replies` | number | Unique reply count | +| ↳ `interested` | string | Interested status | +| ↳ `raw_message_id` | string | Raw email message ID | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Contact Unsubscribed + +Trigger when a contact unsubscribes in Email Bison + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `scheduledEmail` | object | scheduledEmail output from the tool | +| ↳ `id` | number | Scheduled email ID | +| ↳ `lead_id` | number | Lead ID | +| ↳ `sequence_step_id` | number | Sequence step ID | +| ↳ `sequence_step_order` | number | Sequence step order | +| ↳ `sequence_step_variant` | number | Sequence step variant | +| ↳ `email_subject` | string | Email subject | +| ↳ `email_body` | string | Email body HTML | +| ↳ `status` | string | Scheduled email status | +| ↳ `scheduled_date_est` | string | Scheduled date in EST | +| ↳ `scheduled_date_local` | string | Scheduled date in local timezone | +| ↳ `local_timezone` | string | Scheduled email local timezone | +| ↳ `sent_at` | string | Email sent timestamp | +| ↳ `opens` | number | Open count | +| ↳ `replies` | number | Reply count | +| ↳ `unique_opens` | number | Unique open count | +| ↳ `unique_replies` | number | Unique reply count | +| ↳ `interested` | string | Interested status | +| ↳ `raw_message_id` | string | Raw email message ID | +| `campaignEvent` | object | campaignEvent output from the tool | +| ↳ `id` | number | Campaign event ID | +| ↳ `event_type` | string | Campaign event type | +| ↳ `created_at_local` | string | Campaign event local creation timestamp | +| ↳ `local_timezone` | string | Campaign event local timezone | +| ↳ `created_at` | string | Campaign event creation timestamp | +| `lead` | object | lead output from the tool | +| ↳ `id` | number | Lead ID | +| ↳ `email` | string | Lead email address | +| ↳ `first_name` | string | Lead first name | +| ↳ `last_name` | string | Lead last name | +| ↳ `status` | string | Lead status | +| ↳ `title` | string | Lead title | +| ↳ `company` | string | Lead company | +| ↳ `custom_variables` | json | Lead custom variables | +| ↳ `emails_sent` | number | Lead emails sent count | +| ↳ `opens` | number | Lead open count | +| ↳ `unique_opens` | number | Lead unique open count | +| ↳ `replies` | number | Lead reply count | +| ↳ `unique_replies` | number | Lead unique reply count | +| ↳ `bounces` | number | Lead bounce count | +| `campaign` | object | campaign output from the tool | +| ↳ `id` | number | Campaign ID | +| ↳ `name` | string | Campaign name | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Email Account Added + +Trigger when a sender email account is added to Email Bison + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Email Account Disconnected + +Trigger when a sender email account disconnects in Email Bison + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Email Account Reconnected + +Trigger when a sender email account reconnects in Email Bison + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Email Account Removed + +Trigger when a sender email account is removed from Email Bison + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Email Bounced + +Trigger when an Email Bison campaign email bounces + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `reply` | object | reply output from the tool | +| ↳ `id` | number | Reply ID | +| ↳ `uuid` | string | Reply UUID | +| ↳ `email_subject` | string | Reply email subject | +| ↳ `interested` | boolean | Whether the reply is marked interested | +| ↳ `automated_reply` | boolean | Whether the reply is automated | +| ↳ `html_body` | string | Reply HTML body | +| ↳ `text_body` | string | Reply plain text body | +| ↳ `raw_body` | string | Raw MIME reply body | +| ↳ `headers` | string | Encoded raw email headers | +| ↳ `date_received` | string | Reply received timestamp | +| ↳ `from_name` | string | Reply sender name | +| ↳ `from_email_address` | string | Reply sender email address | +| ↳ `primary_to_email_address` | string | Primary recipient email address | +| ↳ `to` | json | Reply To recipients | +| ↳ `cc` | json | Reply CC recipients | +| ↳ `bcc` | json | Reply BCC recipients | +| ↳ `parent_id` | number | Parent reply ID | +| ↳ `reply_type` | string | Reply type | +| ↳ `folder` | string | Reply folder | +| ↳ `raw_message_id` | string | Raw email message ID | +| ↳ `created_at` | string | Reply creation timestamp | +| ↳ `updated_at` | string | Reply update timestamp | +| ↳ `attachments` | json | Reply attachments | +| `campaignEvent` | object | campaignEvent output from the tool | +| ↳ `id` | number | Campaign event ID | +| ↳ `event_type` | string | Campaign event type | +| ↳ `created_at_local` | string | Campaign event local creation timestamp | +| ↳ `local_timezone` | string | Campaign event local timezone | +| ↳ `created_at` | string | Campaign event creation timestamp | +| `lead` | object | lead output from the tool | +| ↳ `id` | number | Lead ID | +| ↳ `email` | string | Lead email address | +| ↳ `first_name` | string | Lead first name | +| ↳ `last_name` | string | Lead last name | +| ↳ `status` | string | Lead status | +| ↳ `title` | string | Lead title | +| ↳ `company` | string | Lead company | +| ↳ `custom_variables` | json | Lead custom variables | +| ↳ `emails_sent` | number | Lead emails sent count | +| ↳ `opens` | number | Lead open count | +| ↳ `unique_opens` | number | Lead unique open count | +| ↳ `replies` | number | Lead reply count | +| ↳ `unique_replies` | number | Lead unique reply count | +| ↳ `bounces` | number | Lead bounce count | +| `campaign` | object | campaign output from the tool | +| ↳ `id` | number | Campaign ID | +| ↳ `name` | string | Campaign name | +| `scheduledEmail` | object | scheduledEmail output from the tool | +| ↳ `id` | number | Scheduled email ID | +| ↳ `sequence_step_id` | number | Sequence step ID | +| ↳ `sequence_step_order` | number | Sequence step order | +| ↳ `sequence_step_variant` | number | Sequence step variant | +| ↳ `status` | string | Scheduled email status | +| ↳ `scheduled_date_est` | string | Scheduled date in EST | +| ↳ `scheduled_date_local` | string | Scheduled date in local timezone | +| ↳ `local_timezone` | string | Scheduled email local timezone | +| ↳ `sent_at` | string | Email sent timestamp | +| ↳ `opens` | number | Open count | +| ↳ `replies` | number | Reply count | +| ↳ `unique_opens` | number | Unique open count | +| ↳ `unique_replies` | number | Unique reply count | +| ↳ `interested` | string | Interested status | +| ↳ `raw_message_id` | string | Raw email message ID | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Email Opened + +Trigger when an Email Bison campaign email is opened + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `scheduledEmail` | object | scheduledEmail output from the tool | +| ↳ `id` | number | Scheduled email ID | +| ↳ `lead_id` | number | Lead ID | +| ↳ `sequence_step_id` | number | Sequence step ID | +| ↳ `sequence_step_order` | number | Sequence step order | +| ↳ `sequence_step_variant` | number | Sequence step variant | +| ↳ `email_subject` | string | Email subject | +| ↳ `email_body` | string | Email body HTML | +| ↳ `status` | string | Scheduled email status | +| ↳ `scheduled_date_est` | string | Scheduled date in EST | +| ↳ `scheduled_date_local` | string | Scheduled date in local timezone | +| ↳ `local_timezone` | string | Scheduled email local timezone | +| ↳ `sent_at` | string | Email sent timestamp | +| ↳ `opens` | number | Open count | +| ↳ `replies` | number | Reply count | +| ↳ `unique_opens` | number | Unique open count | +| ↳ `unique_replies` | number | Unique reply count | +| ↳ `interested` | string | Interested status | +| ↳ `raw_message_id` | string | Raw email message ID | +| `campaignEvent` | object | campaignEvent output from the tool | +| ↳ `id` | number | Campaign event ID | +| ↳ `event_type` | string | Campaign event type | +| ↳ `created_at_local` | string | Campaign event local creation timestamp | +| ↳ `local_timezone` | string | Campaign event local timezone | +| ↳ `created_at` | string | Campaign event creation timestamp | +| `lead` | object | lead output from the tool | +| ↳ `id` | number | Lead ID | +| ↳ `email` | string | Lead email address | +| ↳ `first_name` | string | Lead first name | +| ↳ `last_name` | string | Lead last name | +| ↳ `status` | string | Lead status | +| ↳ `title` | string | Lead title | +| ↳ `company` | string | Lead company | +| ↳ `custom_variables` | json | Lead custom variables | +| ↳ `emails_sent` | number | Lead emails sent count | +| ↳ `opens` | number | Lead open count | +| ↳ `unique_opens` | number | Lead unique open count | +| ↳ `replies` | number | Lead reply count | +| ↳ `unique_replies` | number | Lead unique reply count | +| ↳ `bounces` | number | Lead bounce count | +| `campaign` | object | campaign output from the tool | +| ↳ `id` | number | Campaign ID | +| ↳ `name` | string | Campaign name | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Email Sent + +Trigger when a campaign email is sent in Email Bison + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `scheduledEmail` | object | scheduledEmail output from the tool | +| ↳ `id` | number | Scheduled email ID | +| ↳ `lead_id` | number | Lead ID | +| ↳ `sequence_step_id` | number | Sequence step ID | +| ↳ `sequence_step_order` | number | Sequence step order | +| ↳ `sequence_step_variant` | number | Sequence step variant | +| ↳ `email_subject` | string | Email subject | +| ↳ `email_body` | string | Email body HTML | +| ↳ `status` | string | Scheduled email status | +| ↳ `scheduled_date_est` | string | Scheduled date in EST | +| ↳ `scheduled_date_local` | string | Scheduled date in local timezone | +| ↳ `local_timezone` | string | Scheduled email local timezone | +| ↳ `sent_at` | string | Email sent timestamp | +| ↳ `opens` | number | Open count | +| ↳ `replies` | number | Reply count | +| ↳ `unique_opens` | number | Unique open count | +| ↳ `unique_replies` | number | Unique reply count | +| ↳ `interested` | string | Interested status | +| ↳ `raw_message_id` | string | Raw email message ID | +| `campaignEvent` | object | campaignEvent output from the tool | +| ↳ `id` | number | Campaign event ID | +| ↳ `event_type` | string | Campaign event type | +| ↳ `created_at_local` | string | Campaign event local creation timestamp | +| ↳ `local_timezone` | string | Campaign event local timezone | +| ↳ `created_at` | string | Campaign event creation timestamp | +| `lead` | object | lead output from the tool | +| ↳ `id` | number | Lead ID | +| ↳ `email` | string | Lead email address | +| ↳ `first_name` | string | Lead first name | +| ↳ `last_name` | string | Lead last name | +| ↳ `status` | string | Lead status | +| ↳ `title` | string | Lead title | +| ↳ `company` | string | Lead company | +| ↳ `custom_variables` | json | Lead custom variables | +| ↳ `emails_sent` | number | Lead emails sent count | +| ↳ `opens` | number | Lead open count | +| ↳ `unique_opens` | number | Lead unique open count | +| ↳ `replies` | number | Lead reply count | +| ↳ `unique_replies` | number | Lead unique reply count | +| ↳ `bounces` | number | Lead bounce count | +| `campaign` | object | campaign output from the tool | +| ↳ `id` | number | Campaign ID | +| ↳ `name` | string | Campaign name | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Manual Email Sent + +Trigger when a manual email is sent in Email Bison + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `reply` | object | reply output from the tool | +| ↳ `id` | number | Reply ID | +| ↳ `email_subject` | string | Reply email subject | +| ↳ `interested` | boolean | Whether the reply is marked interested | +| ↳ `automated_reply` | boolean | Whether the reply is automated | +| ↳ `html_body` | string | Reply HTML body | +| ↳ `text_body` | string | Reply plain text body | +| ↳ `raw_body` | string | Raw MIME reply body | +| ↳ `headers` | string | Encoded raw email headers | +| ↳ `date_received` | string | Reply received timestamp | +| ↳ `reply_type` | string | Reply type | +| ↳ `from_name` | string | Reply sender name | +| ↳ `from_email_address` | string | Reply sender email address | +| ↳ `primary_to_email_address` | string | Primary recipient email address | +| ↳ `to` | json | Reply To recipients | +| ↳ `cc` | json | Reply CC recipients | +| ↳ `bcc` | json | Reply BCC recipients | +| ↳ `parent_id` | json | Parent reply ID | +| ↳ `folder` | string | Reply folder | +| ↳ `raw_message_id` | string | Raw email message ID | +| ↳ `created_at` | string | Reply creation timestamp | +| ↳ `updated_at` | string | Reply update timestamp | +| ↳ `attachments` | json | Reply attachments | +| `lead` | object | lead output from the tool | +| ↳ `id` | number | Lead ID | +| ↳ `email` | string | Lead email address | +| ↳ `first_name` | string | Lead first name | +| ↳ `last_name` | string | Lead last name | +| ↳ `status` | string | Lead status | +| ↳ `title` | string | Lead title | +| ↳ `company` | string | Lead company | +| ↳ `custom_variables` | json | Lead custom variables | +| ↳ `emails_sent` | number | Lead emails sent count | +| ↳ `opens` | number | Lead open count | +| ↳ `unique_opens` | number | Lead unique open count | +| ↳ `replies` | number | Lead reply count | +| ↳ `unique_replies` | number | Lead unique reply count | +| ↳ `bounces` | number | Lead bounce count | +| `campaign` | object | campaign output from the tool | +| ↳ `id` | number | Campaign ID | +| ↳ `name` | string | Campaign name | +| `scheduledEmail` | object | scheduledEmail output from the tool | +| ↳ `id` | number | Scheduled email ID | +| ↳ `sequence_step_id` | number | Sequence step ID | +| ↳ `sequence_step_order` | number | Sequence step order | +| ↳ `sequence_step_variant` | number | Sequence step variant | +| ↳ `status` | string | Scheduled email status | +| ↳ `scheduled_date_est` | string | Scheduled date in EST | +| ↳ `scheduled_date_local` | string | Scheduled date in local timezone | +| ↳ `local_timezone` | string | Scheduled email local timezone | +| ↳ `sent_at` | string | Email sent timestamp | +| ↳ `opens` | number | Open count | +| ↳ `replies` | number | Reply count | +| ↳ `unique_opens` | number | Unique open count | +| ↳ `unique_replies` | number | Unique reply count | +| ↳ `interested` | json | Interested status | +| ↳ `raw_message_id` | string | Raw email message ID | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Tag Attached + +Trigger when a custom tag is attached to a taggable in Email Bison + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `tagId` | number | Email Bison tag ID | +| `tagName` | string | Email Bison tag name | +| `taggableId` | number | ID of the tagged resource | +| `taggableType` | string | Type of the tagged resource | + + +--- + +### Email Bison Tag Removed + +Trigger when a custom tag is removed from a taggable in Email Bison + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `tagId` | number | Email Bison tag ID | +| `tagName` | string | Email Bison tag name | +| `taggableId` | number | ID of the tagged resource | +| `taggableType` | string | Type of the tagged resource | + + +--- + +### Email Bison Untracked Reply Received + +Trigger when Email Bison receives a reply not tied to a scheduled campaign email + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `reply` | object | reply output from the tool | +| ↳ `id` | number | Reply ID | +| ↳ `uuid` | string | Reply UUID | +| ↳ `email_subject` | string | Reply email subject | +| ↳ `interested` | boolean | Whether the reply is marked interested | +| ↳ `automated_reply` | boolean | Whether the reply is automated | +| ↳ `html_body` | string | Reply HTML body | +| ↳ `text_body` | string | Reply plain text body | +| ↳ `raw_body` | string | Raw MIME reply body | +| ↳ `headers` | string | Encoded raw email headers | +| ↳ `date_received` | string | Reply received timestamp | +| ↳ `from_name` | string | Reply sender name | +| ↳ `from_email_address` | string | Reply sender email address | +| ↳ `primary_to_email_address` | string | Primary recipient email address | +| ↳ `to` | json | Reply To recipients | +| ↳ `cc` | json | Reply CC recipients | +| ↳ `bcc` | json | Reply BCC recipients | +| ↳ `parent_id` | number | Parent reply ID | +| ↳ `reply_type` | string | Reply type | +| ↳ `folder` | string | Reply folder | +| ↳ `raw_message_id` | string | Raw email message ID | +| ↳ `created_at` | string | Reply creation timestamp | +| ↳ `updated_at` | string | Reply update timestamp | +| ↳ `attachments` | json | Reply attachments | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Warmup Disabled Causing Bounces + +Trigger when warmup is disabled for a sender email causing too many bounces + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + + +--- + +### Email Bison Warmup Disabled Receiving Bounces + +Trigger when warmup is disabled for a sender email receiving too many bounces + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | API Key | +| `apiBaseUrl` | string | Yes | Instance URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Email Bison webhook event type | +| `eventName` | string | Human-readable Email Bison event name | +| `instanceUrl` | string | Email Bison instance URL | +| `workspaceId` | number | Email Bison workspace ID | +| `workspaceName` | string | Email Bison workspace name | +| `event` | json | Raw Email Bison event metadata object | +| `data` | json | Raw Email Bison event data object | +| `senderEmail` | object | senderEmail output from the tool | +| ↳ `id` | number | Sender email ID | +| ↳ `name` | string | Sender email name | +| ↳ `email` | string | Sender email address | +| ↳ `status` | string | Sender email status | +| ↳ `account_type` | string | Sender email connection type | +| ↳ `daily_limit` | number | Sender email daily limit | +| ↳ `emails_sent` | number | Sender email sent count | +| ↳ `replied` | number | Sender email replied count | +| ↳ `opened` | number | Sender email opened count | +| ↳ `unsubscribed` | number | Sender email unsubscribed count | +| ↳ `bounced` | number | Sender email bounced count | +| ↳ `unique_replies` | number | Sender email unique reply count | +| ↳ `unique_opens` | number | Sender email unique open count | +| ↳ `total_leads_contacted` | number | Sender email total leads contacted | +| ↳ `interested` | number | Sender email interested count | +| ↳ `created_at` | string | Sender email creation timestamp | +| ↳ `updated_at` | string | Sender email update timestamp | + diff --git a/apps/docs/content/docs/en/triggers/meta.json b/apps/docs/content/docs/en/triggers/meta.json index 7eff814aad7..70d13afd920 100644 --- a/apps/docs/content/docs/en/triggers/meta.json +++ b/apps/docs/content/docs/en/triggers/meta.json @@ -12,6 +12,7 @@ "calendly", "circleback", "confluence", + "emailbison", "fathom", "fireflies", "github", diff --git a/apps/sim/app/(landing)/integrations/(shell)/[slug]/page.tsx b/apps/sim/app/(landing)/integrations/(shell)/[slug]/page.tsx index 51a6dfa72fe..eb700c9708b 100644 --- a/apps/sim/app/(landing)/integrations/(shell)/[slug]/page.tsx +++ b/apps/sim/app/(landing)/integrations/(shell)/[slug]/page.tsx @@ -75,12 +75,21 @@ const AUTH_STEP: Record = { none: 'Authenticate your account to connect.', } +/** + * Ensures autogenerated prose can be safely composed with a following sentence. + */ +function sentenceWithTerminalPunctuation(value: string): string { + const trimmedValue = value.trim() + return /[.!?]$/.test(trimmedValue) ? trimmedValue : `${trimmedValue}.` +} + /** * Generates targeted FAQs from integration metadata. * Questions mirror real search queries to drive FAQPage rich snippets. */ function buildFAQs(integration: Integration): FAQItem[] { const { name, description, operations, triggers, authType } = integration + const faqDescription = sentenceWithTerminalPunctuation(description) const topOps = operations.slice(0, 5) const topOpNames = topOps.map((o) => o.name) const authStep = AUTH_STEP[authType] @@ -88,7 +97,7 @@ function buildFAQs(integration: Integration): FAQItem[] { const faqs: FAQItem[] = [ { question: `What is Sim's ${name} integration?`, - answer: `Sim's ${name} integration lets you build AI agents that automate tasks in ${name} without writing code. ${description} You can connect ${name} to hundreds of other services in the same agent — from CRMs and spreadsheets to messaging tools and databases.`, + answer: `Sim's ${name} integration lets you build AI agents that automate tasks in ${name} without writing code. ${faqDescription} You can connect ${name} to hundreds of other services in the same agent — from CRMs and spreadsheets to messaging tools and databases.`, }, { question: `What can I automate with ${name} in Sim?`, diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index 9d3280bd825..d1dc93e0a96 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -51,6 +51,7 @@ import { DynamoDBIcon, ElasticsearchIcon, ElevenLabsIcon, + EmailBisonIcon, EnrichSoIcon, EvernoteIcon, ExaAIIcon, @@ -248,6 +249,7 @@ export const blockTypeToIconMap: Record = { dynamodb: DynamoDBIcon, elasticsearch: ElasticsearchIcon, elevenlabs: ElevenLabsIcon, + emailbison: EmailBisonIcon, enrich: EnrichSoIcon, evernote: EvernoteIcon, exa: ExaAIIcon, @@ -361,7 +363,7 @@ export const blockTypeToIconMap: Record = { servicenow: ServiceNowIcon, ses: SESIcon, sftp: SftpIcon, - sharepoint: MicrosoftSharepointIcon, + sharepoint_v2: MicrosoftSharepointIcon, shopify: ShopifyIcon, similarweb: SimilarwebIcon, sixtyfour: SixtyfourIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 7067930cb24..113c411c190 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -3637,6 +3637,77 @@ "integrationTypes": ["ai"], "tags": ["text-to-speech"] }, + { + "type": "emailbison", + "slug": "email-bison", + "name": "Email Bison", + "description": "Manage Email Bison leads, campaigns, replies, and tags", + "longDescription": "Integrate Email Bison into workflows. Create and update leads, manage campaigns, attach leads to campaigns, list replies, and organize leads with tags.", + "bgColor": "#FB7A22", + "iconName": "EmailBisonIcon", + "docsUrl": "https://docs.sim.ai/tools/emailbison", + "operations": [ + { + "name": "List Leads", + "description": "Retrieves leads from Email Bison with optional search and tag filters." + }, + { + "name": "Get Lead", + "description": "Retrieves a lead by Email Bison lead ID or email address." + }, + { + "name": "Create Lead", + "description": "Creates a single lead in Email Bison." + }, + { + "name": "Update Lead", + "description": "Updates an existing Email Bison lead. Fields omitted from a PUT update may be cleared by Email Bison." + }, + { + "name": "List Campaigns", + "description": "Retrieves Email Bison campaigns." + }, + { + "name": "Create Campaign", + "description": "Creates a new Email Bison campaign." + }, + { + "name": "Update Campaign", + "description": "Updates Email Bison campaign settings." + }, + { + "name": "Update Campaign Status", + "description": "Pauses, resumes, or archives an Email Bison campaign." + }, + { + "name": "Attach Leads to Campaign", + "description": "Adds existing Email Bison leads to a campaign." + }, + { + "name": "List Replies", + "description": "Retrieves Email Bison replies with optional status, folder, campaign, sender, lead, and tag filters." + }, + { + "name": "List Tags", + "description": "Retrieves all Email Bison tags for the authenticated workspace." + }, + { + "name": "Create Tag", + "description": "Creates a new Email Bison tag." + }, + { + "name": "Attach Tags to Leads", + "description": "Attaches Email Bison tags to one or more leads." + } + ], + "operationCount": 13, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationTypes": ["email", "developer-tools", "sales"], + "tags": ["sales-engagement", "email-marketing", "automation"] + }, { "type": "openai", "slug": "embeddings", @@ -11873,9 +11944,9 @@ "tags": ["cloud", "automation"] }, { - "type": "sharepoint", + "type": "sharepoint_v2", "slug": "sharepoint", - "name": "Sharepoint", + "name": "SharePoint", "description": "Work with pages and lists", "longDescription": "Integrate SharePoint into the workflow. Read/create pages, list sites, and work with lists (read, create, update items). Requires OAuth.", "bgColor": "#E0E0E0", @@ -11903,11 +11974,11 @@ "description": "Get metadata (and optionally columns/items) for a SharePoint list" }, { - "name": "Update List", + "name": "Update List Item", "description": "Update the properties (fields) on a SharePoint list item" }, { - "name": "Add List Items", + "name": "Add List Item", "description": "Add a new item to a SharePoint list" }, { diff --git a/apps/sim/app/api/tools/sharepoint/upload/route.ts b/apps/sim/app/api/tools/sharepoint/upload/route.ts index 20a735a1de9..556de6d4225 100644 --- a/apps/sim/app/api/tools/sharepoint/upload/route.ts +++ b/apps/sim/app/api/tools/sharepoint/upload/route.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { sharepointUploadContract } from '@/lib/api/contracts/tools/microsoft' import { parseRequest } from '@/lib/api/server' @@ -9,10 +10,12 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import type { MicrosoftGraphDriveItem } from '@/tools/onedrive/types' +import type { SharepointSkippedFile, SharepointUploadError } from '@/tools/sharepoint/types' export const dynamic = 'force-dynamic' const logger = createLogger('SharepointUploadAPI') +const MAX_SHAREPOINT_UPLOAD_BYTES = 250 * 1024 * 1024 export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() @@ -72,42 +75,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } - let effectiveDriveId = validatedData.driveId - if (!effectiveDriveId) { - logger.info(`[${requestId}] No driveId provided, fetching default drive for site`) - const driveUrl = `https://graph.microsoft.com/v1.0/sites/${validatedData.siteId}/drive` - const driveResponse = await secureFetchWithValidation( - driveUrl, - { - method: 'GET', - headers: { - Authorization: `Bearer ${validatedData.accessToken}`, - Accept: 'application/json', - }, - }, - 'driveUrl' - ) - - if (!driveResponse.ok) { - const errorData = (await driveResponse.json().catch(() => ({}))) as { - error?: { message?: string } - } - logger.error(`[${requestId}] Failed to get default drive:`, errorData) - return NextResponse.json( - { - success: false, - error: errorData.error?.message || 'Failed to get default document library', - }, - { status: driveResponse.status } - ) - } - - const driveData = (await driveResponse.json()) as { id: string } - effectiveDriveId = driveData.id - logger.info(`[${requestId}] Using default drive: ${effectiveDriveId}`) - } - - const uploadedFiles: any[] = [] + const siteId = validatedData.siteId.trim() || 'root' + const driveId = validatedData.driveId?.trim() || null + const uploadedFiles: MicrosoftGraphDriveItem[] = [] + const skippedFiles: SharepointSkippedFile[] = [] + const errors: SharepointUploadError[] = [] for (const userFile of userFiles) { logger.info(`[${requestId}] Uploading file: ${userFile.name}`) @@ -119,10 +91,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const fileSizeMB = buffer.length / (1024 * 1024) - if (fileSizeMB > 250) { + if (buffer.length > MAX_SHAREPOINT_UPLOAD_BYTES) { logger.warn( `[${requestId}] File ${fileName} is ${fileSizeMB.toFixed(2)}MB, exceeds 250MB limit` ) + skippedFiles.push({ + name: fileName, + size: buffer.length, + limit: MAX_SHAREPOINT_UPLOAD_BYTES, + reason: 'File exceeds the 250 MB Microsoft Graph small upload limit', + }) continue } @@ -142,7 +120,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { .map((segment) => (segment ? encodeURIComponent(segment) : '')) .join('/') - const uploadUrl = `https://graph.microsoft.com/v1.0/sites/${validatedData.siteId}/drives/${effectiveDriveId}/root:${encodedPath}:/content` + const uploadUrl = driveId + ? `https://graph.microsoft.com/v1.0/drives/${driveId}/root:${encodedPath}:/content` + : `https://graph.microsoft.com/v1.0/sites/${siteId}/drive/root:${encodedPath}:/content` logger.info(`[${requestId}] Uploading to: ${uploadUrl}`) @@ -185,13 +165,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => { error?: { message?: string } } logger.error(`[${requestId}] Failed to replace file ${fileName}:`, replaceErrorData) - return NextResponse.json( - { - success: false, - error: replaceErrorData.error?.message || `Failed to replace file: ${fileName}`, - }, - { status: replaceResponse.status } - ) + errors.push({ + name: fileName, + status: replaceResponse.status, + error: replaceErrorData.error?.message || `Failed to replace file: ${fileName}`, + }) + continue } const replaceData = (await replaceResponse.json()) as { @@ -215,15 +194,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => { continue } - return NextResponse.json( - { - success: false, - error: - (errorData as { error?: { message?: string } }).error?.message || - `Failed to upload file: ${fileName}`, - }, - { status: uploadResponse.status } - ) + errors.push({ + name: fileName, + status: uploadResponse.status, + error: + (errorData as { error?: { message?: string } }).error?.message || + `Failed to upload file: ${fileName}`, + }) + continue } const uploadData = (await uploadResponse.json()) as MicrosoftGraphDriveItem @@ -240,22 +218,33 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } if (uploadedFiles.length === 0) { - return NextResponse.json( - { - success: false, - error: 'No files were uploaded successfully', + return NextResponse.json({ + success: false, + error: 'No files were uploaded successfully', + output: { + uploadedFiles, + fileCount: 0, + skippedFiles, + skippedCount: skippedFiles.length, + errors, }, - { status: 400 } - ) + }) } - logger.info(`[${requestId}] Successfully uploaded ${uploadedFiles.length} file(s)`) + logger.info(`[${requestId}] Completed SharePoint upload`, { + uploadedCount: uploadedFiles.length, + skippedCount: skippedFiles.length, + errorCount: errors.length, + }) return NextResponse.json({ success: true, output: { uploadedFiles, fileCount: uploadedFiles.length, + skippedFiles, + skippedCount: skippedFiles.length, + errors, }, }) } catch (error) { @@ -263,7 +252,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', + error: toError(error).message, }, { status: 500 } ) diff --git a/apps/sim/blocks/blocks/emailbison.ts b/apps/sim/blocks/blocks/emailbison.ts new file mode 100644 index 00000000000..b253703b16b --- /dev/null +++ b/apps/sim/blocks/blocks/emailbison.ts @@ -0,0 +1,627 @@ +import { EmailBisonIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import type { EmailBisonResponse } from '@/tools/emailbison/types' +import { getTrigger } from '@/triggers' + +const LEAD_MUTATION_OPERATIONS = ['create_lead', 'update_lead'] as const +const LEAD_LIST_OPERATIONS = ['list_leads'] as const +const CAMPAIGN_ID_OPERATIONS = [ + 'update_campaign', + 'update_campaign_status', + 'attach_leads_to_campaign', +] as const +const REPLY_FILTER_OPERATIONS = ['list_replies'] as const +const TAG_FILTER_OPERATIONS = ['list_leads', 'list_replies'] as const +const EMAILBISON_TRIGGER_IDS = [ + 'emailbison_email_sent', + 'emailbison_lead_first_contacted', + 'emailbison_lead_replied', + 'emailbison_lead_interested', + 'emailbison_lead_unsubscribed', + 'emailbison_untracked_reply_received', + 'emailbison_email_opened', + 'emailbison_email_bounced', + 'emailbison_email_account_added', + 'emailbison_email_account_removed', + 'emailbison_email_account_disconnected', + 'emailbison_email_account_reconnected', + 'emailbison_manual_email_sent', + 'emailbison_tag_attached', + 'emailbison_tag_removed', + 'emailbison_warmup_disabled_receiving_bounces', + 'emailbison_warmup_disabled_causing_bounces', +] as const + +export const EmailBisonBlock: BlockConfig = { + type: 'emailbison', + name: 'Email Bison', + description: 'Manage Email Bison leads, campaigns, replies, and tags', + longDescription: + 'Integrate Email Bison into workflows. Create and update leads, manage campaigns, attach leads to campaigns, list replies, and organize leads with tags.', + docsLink: 'https://docs.sim.ai/tools/emailbison', + category: 'tools', + integrationType: IntegrationType.Email, + tags: ['sales-engagement', 'email-marketing', 'automation'], + bgColor: '#FB7A22', + icon: EmailBisonIcon, + authMode: AuthMode.ApiKey, + triggers: { + enabled: true, + available: [...EMAILBISON_TRIGGER_IDS], + }, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'List Leads', id: 'list_leads' }, + { label: 'Get Lead', id: 'get_lead' }, + { label: 'Create Lead', id: 'create_lead' }, + { label: 'Update Lead', id: 'update_lead' }, + { label: 'List Campaigns', id: 'list_campaigns' }, + { label: 'Create Campaign', id: 'create_campaign' }, + { label: 'Update Campaign', id: 'update_campaign' }, + { label: 'Update Campaign Status', id: 'update_campaign_status' }, + { label: 'Attach Leads to Campaign', id: 'attach_leads_to_campaign' }, + { label: 'List Replies', id: 'list_replies' }, + { label: 'List Tags', id: 'list_tags' }, + { label: 'Create Tag', id: 'create_tag' }, + { label: 'Attach Tags to Leads', id: 'attach_tags_to_leads' }, + ], + value: () => 'list_leads', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + password: true, + placeholder: 'Enter your Email Bison API token', + required: true, + }, + { + id: 'apiBaseUrl', + title: 'Instance URL', + type: 'short-input', + placeholder: 'https://your-emailbison-workspace.com', + required: true, + }, + { + id: 'leadId', + title: 'Lead ID or Email', + type: 'short-input', + placeholder: 'Lead ID or email address', + required: { field: 'operation', value: ['get_lead', 'update_lead'] }, + condition: { field: 'operation', value: ['get_lead', 'update_lead'] }, + }, + { + id: 'search', + title: 'Search', + type: 'short-input', + placeholder: 'Search term', + condition: { + field: 'operation', + value: [...LEAD_LIST_OPERATIONS, ...REPLY_FILTER_OPERATIONS], + }, + }, + { + id: 'campaignStatus', + title: 'Lead Campaign Status', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'In Sequence', id: 'in_sequence' }, + { label: 'Sequence Finished', id: 'sequence_finished' }, + { label: 'Sequence Stopped', id: 'sequence_stopped' }, + { label: 'Never Contacted', id: 'never_contacted' }, + { label: 'Replied', id: 'replied' }, + ], + value: () => '', + condition: { field: 'operation', value: 'list_leads' }, + mode: 'advanced', + }, + { + id: 'firstName', + title: 'First Name', + type: 'short-input', + placeholder: 'John', + required: { field: 'operation', value: [...LEAD_MUTATION_OPERATIONS] }, + condition: { field: 'operation', value: [...LEAD_MUTATION_OPERATIONS] }, + }, + { + id: 'lastName', + title: 'Last Name', + type: 'short-input', + placeholder: 'Doe', + required: { field: 'operation', value: [...LEAD_MUTATION_OPERATIONS] }, + condition: { field: 'operation', value: [...LEAD_MUTATION_OPERATIONS] }, + }, + { + id: 'email', + title: 'Email', + type: 'short-input', + placeholder: 'john@example.com', + required: { field: 'operation', value: [...LEAD_MUTATION_OPERATIONS] }, + condition: { field: 'operation', value: [...LEAD_MUTATION_OPERATIONS] }, + }, + { + id: 'title', + title: 'Title', + type: 'short-input', + placeholder: 'Engineer', + condition: { field: 'operation', value: [...LEAD_MUTATION_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'company', + title: 'Company', + type: 'short-input', + placeholder: 'Acme Inc.', + condition: { field: 'operation', value: [...LEAD_MUTATION_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'notes', + title: 'Notes', + type: 'long-input', + placeholder: 'Additional notes', + condition: { field: 'operation', value: [...LEAD_MUTATION_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'customVariables', + title: 'Custom Variables', + type: 'long-input', + placeholder: '[{"name":"linkedin_url","value":"https://linkedin.com/in/john"}]', + condition: { field: 'operation', value: [...LEAD_MUTATION_OPERATIONS] }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of Email Bison custom variables. Each item must have name and value. Return ONLY the JSON array.', + generationType: 'json-object', + }, + mode: 'advanced', + }, + { + id: 'campaignId', + title: 'Campaign ID', + type: 'short-input', + placeholder: '123', + required: { field: 'operation', value: [...CAMPAIGN_ID_OPERATIONS] }, + condition: { + field: 'operation', + value: [...CAMPAIGN_ID_OPERATIONS, ...REPLY_FILTER_OPERATIONS], + }, + }, + { + id: 'campaignName', + title: 'Campaign Name', + type: 'short-input', + placeholder: 'Outbound Campaign', + required: { field: 'operation', value: 'create_campaign' }, + condition: { field: 'operation', value: ['create_campaign', 'update_campaign'] }, + }, + { + id: 'campaignType', + title: 'Campaign Type', + type: 'dropdown', + options: [ + { label: 'Outbound', id: 'outbound' }, + { label: 'Reply Follow-up', id: 'reply_followup' }, + ], + value: () => 'outbound', + condition: { field: 'operation', value: 'create_campaign' }, + mode: 'advanced', + }, + { + id: 'action', + title: 'Status Action', + type: 'dropdown', + options: [ + { label: 'Pause', id: 'pause' }, + { label: 'Resume', id: 'resume' }, + { label: 'Archive', id: 'archive' }, + ], + value: () => 'pause', + required: { field: 'operation', value: 'update_campaign_status' }, + condition: { field: 'operation', value: 'update_campaign_status' }, + }, + { + id: 'maxEmailsPerDay', + title: 'Max Emails Per Day', + type: 'short-input', + placeholder: '500', + condition: { field: 'operation', value: 'update_campaign' }, + mode: 'advanced', + }, + { + id: 'maxNewLeadsPerDay', + title: 'Max New Leads Per Day', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: 'update_campaign' }, + mode: 'advanced', + }, + { + id: 'sequencePrioritization', + title: 'Sequence Prioritization', + type: 'dropdown', + options: [ + { label: 'Unchanged', id: '' }, + { label: 'Follow-ups', id: 'followups' }, + { label: 'New Leads', id: 'new_leads' }, + ], + value: () => '', + condition: { field: 'operation', value: 'update_campaign' }, + mode: 'advanced', + }, + { + id: 'plainText', + title: 'Plain Text', + type: 'dropdown', + options: [ + { label: 'Unchanged', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: 'update_campaign' }, + mode: 'advanced', + }, + { + id: 'openTracking', + title: 'Open Tracking', + type: 'dropdown', + options: [ + { label: 'Unchanged', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: 'update_campaign' }, + mode: 'advanced', + }, + { + id: 'reputationBuilding', + title: 'Reputation Building', + type: 'dropdown', + options: [ + { label: 'Unchanged', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: 'update_campaign' }, + mode: 'advanced', + }, + { + id: 'canUnsubscribe', + title: 'Can Unsubscribe', + type: 'dropdown', + options: [ + { label: 'Unchanged', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: 'update_campaign' }, + mode: 'advanced', + }, + { + id: 'includeAutoRepliesInStats', + title: 'Include Auto Replies in Stats', + type: 'dropdown', + options: [ + { label: 'Unchanged', id: '' }, + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: 'update_campaign' }, + mode: 'advanced', + }, + { + id: 'leadIds', + title: 'Lead IDs', + type: 'short-input', + placeholder: '1,2,3', + required: { field: 'operation', value: ['attach_leads_to_campaign', 'attach_tags_to_leads'] }, + condition: { + field: 'operation', + value: ['attach_leads_to_campaign', 'attach_tags_to_leads'], + }, + }, + { + id: 'allowParallelSending', + title: 'Allow Parallel Sending', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'attach_leads_to_campaign' }, + mode: 'advanced', + }, + { + id: 'replyStatus', + title: 'Reply Status', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'Interested', id: 'interested' }, + { label: 'Automated Reply', id: 'automated_reply' }, + { label: 'Not Automated Reply', id: 'not_automated_reply' }, + ], + value: () => '', + condition: { field: 'operation', value: 'list_replies' }, + mode: 'advanced', + }, + { + id: 'folder', + title: 'Folder', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'Inbox', id: 'inbox' }, + { label: 'Sent', id: 'sent' }, + { label: 'Spam', id: 'spam' }, + { label: 'Bounced', id: 'bounced' }, + { label: 'All', id: 'all' }, + ], + value: () => '', + condition: { field: 'operation', value: 'list_replies' }, + mode: 'advanced', + }, + { + id: 'read', + title: 'Read', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'Read', id: 'true' }, + { label: 'Unread', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: 'list_replies' }, + mode: 'advanced', + }, + { + id: 'senderEmailId', + title: 'Sender Email ID', + type: 'short-input', + placeholder: '243', + condition: { field: 'operation', value: 'list_replies' }, + mode: 'advanced', + }, + { + id: 'replyLeadId', + title: 'Lead ID', + type: 'short-input', + placeholder: '14', + condition: { field: 'operation', value: 'list_replies' }, + mode: 'advanced', + }, + { + id: 'tagName', + title: 'Tag Name', + type: 'short-input', + placeholder: 'Interested', + required: { field: 'operation', value: 'create_tag' }, + condition: { field: 'operation', value: 'create_tag' }, + }, + { + id: 'tagIds', + title: 'Tag IDs', + type: 'short-input', + placeholder: '1,2,3', + required: { field: 'operation', value: 'attach_tags_to_leads' }, + condition: { field: 'operation', value: 'attach_tags_to_leads' }, + }, + { + id: 'filterTagIds', + title: 'Filter Tag IDs', + type: 'short-input', + placeholder: '1,2,3', + condition: { field: 'operation', value: [...TAG_FILTER_OPERATIONS] }, + mode: 'advanced', + }, + { + id: 'excludedTagIds', + title: 'Excluded Tag IDs', + type: 'short-input', + placeholder: '4,5,6', + condition: { field: 'operation', value: 'list_leads' }, + mode: 'advanced', + }, + { + id: 'withoutTags', + title: 'Without Tags', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'list_leads' }, + mode: 'advanced', + }, + { + id: 'skipWebhooks', + title: 'Skip Webhooks', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'attach_tags_to_leads' }, + mode: 'advanced', + }, + ...EMAILBISON_TRIGGER_IDS.flatMap((triggerId) => getTrigger(triggerId).subBlocks), + ], + tools: { + access: [ + 'emailbison_list_leads', + 'emailbison_get_lead', + 'emailbison_create_lead', + 'emailbison_update_lead', + 'emailbison_list_campaigns', + 'emailbison_create_campaign', + 'emailbison_update_campaign', + 'emailbison_update_campaign_status', + 'emailbison_attach_leads_to_campaign', + 'emailbison_list_replies', + 'emailbison_list_tags', + 'emailbison_create_tag', + 'emailbison_attach_tags_to_leads', + ], + config: { + tool: (params) => `emailbison_${params.operation}`, + params: (params) => ({ + apiBaseUrl: params.apiBaseUrl, + leadId: + params.operation === 'list_replies' ? toNumberParam(params.replyLeadId) : params.leadId, + leadIds: parseNumberList(params.leadIds), + tagIds: parseNumberList( + params.operation === 'attach_tags_to_leads' ? params.tagIds : params.filterTagIds + ), + excludedTagIds: parseNumberList(params.excludedTagIds), + customVariables: parseJsonArray(params.customVariables), + campaignId: toNumberParam(params.campaignId), + campaignType: emptyToUndefined(params.campaignType), + name: + params.operation === 'create_tag' || params.operation === 'create_campaign' + ? params.operation === 'create_tag' + ? params.tagName + : params.campaignName + : emptyToUndefined(params.campaignName), + action: params.action, + maxEmailsPerDay: toNumberParam(params.maxEmailsPerDay), + maxNewLeadsPerDay: toNumberParam(params.maxNewLeadsPerDay), + sequencePrioritization: emptyToUndefined(params.sequencePrioritization), + plainText: toBooleanParam(params.plainText), + openTracking: toBooleanParam(params.openTracking), + reputationBuilding: toBooleanParam(params.reputationBuilding), + canUnsubscribe: toBooleanParam(params.canUnsubscribe), + includeAutoRepliesInStats: toBooleanParam(params.includeAutoRepliesInStats), + allowParallelSending: toBooleanParam(params.allowParallelSending), + skipWebhooks: toBooleanParam(params.skipWebhooks), + withoutTags: toBooleanParam(params.withoutTags), + read: toBooleanParam(params.read), + senderEmailId: toNumberParam(params.senderEmailId), + campaignStatus: emptyToUndefined(params.campaignStatus), + status: + params.operation === 'list_replies' ? emptyToUndefined(params.replyStatus) : undefined, + }), + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Email Bison API token' }, + apiBaseUrl: { type: 'string', description: 'Email Bison instance URL' }, + leadId: { type: 'string', description: 'Lead ID or email address' }, + search: { type: 'string', description: 'Search term' }, + campaignStatus: { type: 'string', description: 'Lead campaign status filter' }, + firstName: { type: 'string', description: 'Lead first name' }, + lastName: { type: 'string', description: 'Lead last name' }, + email: { type: 'string', description: 'Lead email address' }, + title: { type: 'string', description: 'Lead title' }, + company: { type: 'string', description: 'Lead company' }, + notes: { type: 'string', description: 'Lead notes' }, + customVariables: { type: 'array', description: 'Lead custom variables' }, + campaignId: { type: 'number', description: 'Campaign ID' }, + campaignName: { type: 'string', description: 'Campaign name' }, + campaignType: { type: 'string', description: 'Campaign type' }, + action: { type: 'string', description: 'Campaign status action' }, + maxEmailsPerDay: { type: 'number', description: 'Maximum emails per day' }, + maxNewLeadsPerDay: { type: 'number', description: 'Maximum new leads per day' }, + sequencePrioritization: { type: 'string', description: 'Campaign sequence prioritization' }, + plainText: { type: 'boolean', description: 'Whether campaign emails should be plain text' }, + openTracking: { type: 'boolean', description: 'Whether open tracking should be enabled' }, + reputationBuilding: { type: 'boolean', description: 'Whether reputation building is enabled' }, + canUnsubscribe: { type: 'boolean', description: 'Whether recipients can unsubscribe' }, + includeAutoRepliesInStats: { + type: 'boolean', + description: 'Whether auto replies are included in campaign stats', + }, + leadIds: { type: 'array', description: 'Lead IDs' }, + allowParallelSending: { type: 'boolean', description: 'Allow parallel sending' }, + replyStatus: { type: 'string', description: 'Reply status filter' }, + folder: { type: 'string', description: 'Reply folder filter' }, + read: { type: 'boolean', description: 'Reply read filter' }, + senderEmailId: { type: 'number', description: 'Sender email ID' }, + replyLeadId: { type: 'number', description: 'Reply lead ID filter' }, + tagName: { type: 'string', description: 'Tag name' }, + tagIds: { type: 'array', description: 'Tag IDs' }, + filterTagIds: { type: 'array', description: 'Tag IDs to filter by' }, + excludedTagIds: { type: 'array', description: 'Excluded tag IDs' }, + withoutTags: { type: 'boolean', description: 'Only include leads without tags' }, + skipWebhooks: { type: 'boolean', description: 'Skip Email Bison webhooks' }, + }, + outputs: { + leads: { type: 'array', description: 'List of leads' }, + campaigns: { type: 'array', description: 'List of campaigns' }, + replies: { type: 'array', description: 'List of replies' }, + tags: { type: 'array', description: 'List of tags' }, + count: { type: 'number', description: 'Number of returned records' }, + id: { type: 'number', description: 'Record ID' }, + uuid: { type: 'string', description: 'Record UUID' }, + name: { type: 'string', description: 'Campaign or tag name' }, + first_name: { type: 'string', description: 'Lead first name' }, + last_name: { type: 'string', description: 'Lead last name' }, + email: { type: 'string', description: 'Lead email address' }, + status: { type: 'string', description: 'Record status' }, + success: { type: 'boolean', description: 'Whether the action succeeded' }, + message: { type: 'string', description: 'Action message' }, + }, +} + +function parseNumberList(value: unknown): number[] | undefined { + if (Array.isArray(value)) { + const numbers = value.map(toNumberParam).filter((number) => number !== undefined) + return numbers.length > 0 ? numbers : undefined + } + + if (typeof value !== 'string') return undefined + + const numbers = value + .split(/[\s,]+/) + .map(toNumberParam) + .filter((number) => number !== undefined) + + return numbers.length > 0 ? numbers : undefined +} + +function parseJsonArray(value: unknown): unknown[] | undefined { + if (Array.isArray(value)) return value + if (typeof value !== 'string' || value.trim() === '') return undefined + + try { + const parsed: unknown = JSON.parse(value) + return Array.isArray(parsed) ? parsed : undefined + } catch { + return undefined + } +} + +function toNumberParam(value: unknown): number | undefined { + if (typeof value === 'number') return Number.isFinite(value) ? value : undefined + if (typeof value !== 'string' || value.trim() === '') return undefined + + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined +} + +function toBooleanParam(value: unknown): boolean | undefined { + if (typeof value === 'boolean') return value + if (typeof value !== 'string' || value.trim() === '') return undefined + if (value === 'true') return true + if (value === 'false') return false + return undefined +} + +function emptyToUndefined(value: unknown): unknown { + return value === '' ? undefined : value +} diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 5698d544d85..b881c46e24a 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -14,6 +14,7 @@ export const SharepointBlock: BlockConfig = { name: 'Sharepoint', description: 'Work with pages and lists', authMode: AuthMode.OAuth, + hideFromToolbar: true, longDescription: 'Integrate SharePoint into the workflow. Read/create pages, list sites, and work with lists (read, create, update items). Requires OAuth.', docsLink: 'https://docs.sim.ai/tools/sharepoint', @@ -567,3 +568,485 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, }, }, } + +const SHAREPOINT_V2_TOOL_IDS = [ + 'sharepoint_create_page', + 'sharepoint_read_page', + 'sharepoint_list_sites', + 'sharepoint_create_list', + 'sharepoint_get_list', + 'sharepoint_update_list', + 'sharepoint_add_list_items', + 'sharepoint_upload_file', +] as const + +const SHAREPOINT_V2_SITE_OPERATIONS = Array.from(SHAREPOINT_V2_TOOL_IDS) + +const SHAREPOINT_V2_LIST_ITEM_OPERATIONS = [ + 'sharepoint_update_list', + 'sharepoint_add_list_items', +] as const + +export const SharepointV2Block: BlockConfig = { + ...SharepointBlock, + type: 'sharepoint_v2', + name: 'SharePoint', + hideFromToolbar: false, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create Page', id: 'sharepoint_create_page' }, + { label: 'Read Page', id: 'sharepoint_read_page' }, + { label: 'List Sites', id: 'sharepoint_list_sites' }, + { label: 'Create List', id: 'sharepoint_create_list' }, + { label: 'Read List', id: 'sharepoint_get_list' }, + { label: 'Update List Item', id: 'sharepoint_update_list' }, + { label: 'Add List Item', id: 'sharepoint_add_list_items' }, + { label: 'Upload File', id: 'sharepoint_upload_file' }, + ], + value: () => 'sharepoint_create_page', + }, + { + id: 'credential', + title: 'Microsoft Account', + type: 'oauth-input', + canonicalParamId: 'oauthCredential', + mode: 'basic', + serviceId: 'sharepoint', + requiredScopes: getScopesForService('sharepoint'), + placeholder: 'Select Microsoft account', + required: true, + }, + { + id: 'manualCredential', + title: 'Microsoft Account', + type: 'short-input', + canonicalParamId: 'oauthCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, + }, + { + id: 'siteSelector', + title: 'Select Site', + type: 'file-selector', + canonicalParamId: 'siteId', + serviceId: 'sharepoint', + selectorKey: 'sharepoint.sites', + requiredScopes: getScopesForService('sharepoint'), + mimeType: 'application/vnd.microsoft.graph.site', + placeholder: 'Select a site', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: SHAREPOINT_V2_SITE_OPERATIONS }, + }, + { + id: 'manualSiteId', + title: 'Site ID', + type: 'short-input', + canonicalParamId: 'siteId', + placeholder: 'Enter site ID (leave empty for root site)', + dependsOn: ['credential'], + mode: 'advanced', + condition: { field: 'operation', value: SHAREPOINT_V2_SITE_OPERATIONS }, + }, + { + id: 'pageName', + title: 'Page Name', + type: 'short-input', + placeholder: 'Name of the page', + condition: { field: 'operation', value: ['sharepoint_create_page', 'sharepoint_read_page'] }, + required: { field: 'operation', value: 'sharepoint_create_page' }, + }, + { + id: 'pageId', + title: 'Page ID', + type: 'short-input', + placeholder: 'Page ID (alternative to page name)', + condition: { field: 'operation', value: 'sharepoint_read_page' }, + mode: 'advanced', + }, + { + id: 'pageTitle', + title: 'Page Title', + type: 'short-input', + placeholder: 'Optional title (defaults to page name)', + condition: { field: 'operation', value: 'sharepoint_create_page' }, + mode: 'advanced', + }, + { + id: 'pageContent', + title: 'Page Content', + type: 'long-input', + placeholder: 'Optional text content for the page', + condition: { field: 'operation', value: 'sharepoint_create_page' }, + mode: 'advanced', + }, + { + id: 'maxPages', + title: 'Max Pages', + type: 'short-input', + placeholder: 'Default 10, maximum 50', + condition: { field: 'operation', value: 'sharepoint_read_page' }, + mode: 'advanced', + }, + { + id: 'groupId', + title: 'Group ID', + type: 'short-input', + placeholder: 'Optional Microsoft 365 group ID', + condition: { field: 'operation', value: 'sharepoint_list_sites' }, + mode: 'advanced', + }, + { + id: 'listSelector', + title: 'List', + type: 'file-selector', + canonicalParamId: 'listId', + serviceId: 'sharepoint', + selectorKey: 'sharepoint.lists', + placeholder: 'Select a list', + dependsOn: ['credential', 'siteSelector'], + mode: 'basic', + condition: { + field: 'operation', + value: ['sharepoint_get_list', ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS], + }, + required: { field: 'operation', value: [...SHAREPOINT_V2_LIST_ITEM_OPERATIONS] }, + }, + { + id: 'manualListId', + title: 'List ID', + type: 'short-input', + canonicalParamId: 'listId', + placeholder: 'Enter list ID (GUID). Required for list item operations.', + mode: 'advanced', + condition: { + field: 'operation', + value: ['sharepoint_get_list', ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS], + }, + required: { field: 'operation', value: [...SHAREPOINT_V2_LIST_ITEM_OPERATIONS] }, + }, + { + id: 'includeColumns', + title: 'Include Columns', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'sharepoint_get_list' }, + mode: 'advanced', + }, + { + id: 'includeItems', + title: 'Include Items', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => 'true', + condition: { field: 'operation', value: 'sharepoint_get_list' }, + mode: 'advanced', + }, + { + id: 'listDisplayName', + title: 'List Display Name', + type: 'short-input', + placeholder: 'Name of the list', + condition: { field: 'operation', value: 'sharepoint_create_list' }, + required: { field: 'operation', value: 'sharepoint_create_list' }, + }, + { + id: 'listTemplate', + title: 'List Template', + type: 'short-input', + placeholder: "Template (e.g., 'genericList')", + condition: { field: 'operation', value: 'sharepoint_create_list' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a SharePoint list template name based on the user's description. + +### AVAILABLE TEMPLATES +- genericList - Standard list for general data (default) +- documentLibrary - For storing and managing documents +- survey - For creating surveys and polls +- links - For storing hyperlinks +- announcements - For news and announcements +- contacts - For contact information (name, email, phone) +- events - For calendar events and scheduling +- tasks - For task tracking and project management +- discussionBoard - For team discussions and forums +- pictureLibrary - For storing images and photos +- issue - For issue/bug tracking + +Return ONLY the template name - no explanations, no quotes, no extra text.`, + placeholder: 'Describe what kind of list you need...', + }, + }, + { + id: 'columnDefinitions', + title: 'Column Definitions', + type: 'long-input', + placeholder: 'Optional: Define custom columns as JSON array', + condition: { field: 'operation', value: 'sharepoint_create_list' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a JSON array of SharePoint list column definitions based on the user's description. + +Each column needs at minimum a "name" and column type properties, for example: +[{"name": "Status", "choice": {"choices": ["Active", "Completed"]}}, {"name": "DueDate", "dateTime": {"format": "dateOnly"}}] + +Return ONLY the JSON array - no explanations, no markdown, no extra text.`, + placeholder: 'Describe the columns you want to add...', + generationType: 'json-object', + }, + }, + { + id: 'listDescription', + title: 'List Description', + type: 'long-input', + placeholder: 'Optional description', + condition: { field: 'operation', value: 'sharepoint_create_list' }, + mode: 'advanced', + }, + { + id: 'itemId', + title: 'Item ID', + type: 'short-input', + placeholder: 'Enter item ID', + condition: { field: 'operation', value: 'sharepoint_update_list' }, + required: { field: 'operation', value: 'sharepoint_update_list' }, + }, + { + id: 'listItemFields', + title: 'List Item Fields', + type: 'long-input', + placeholder: + 'Enter list item fields as JSON (e.g., {"Title": "My Item", "Status": "Active"})', + condition: { field: 'operation', value: [...SHAREPOINT_V2_LIST_ITEM_OPERATIONS] }, + required: { field: 'operation', value: [...SHAREPOINT_V2_LIST_ITEM_OPERATIONS] }, + wandConfig: { + enabled: true, + prompt: `Generate a JSON object for SharePoint list item fields based on the user's description. + +Use column internal names as keys and valid field values as values. + +Return ONLY the JSON object - no explanations, no markdown, no extra text.`, + placeholder: 'Describe the fields and values you want to set...', + generationType: 'json-object', + }, + }, + { + id: 'driveId', + title: 'Document Library ID', + type: 'short-input', + placeholder: 'Enter document library (drive) ID', + condition: { field: 'operation', value: 'sharepoint_upload_file' }, + mode: 'advanced', + }, + { + id: 'folderPath', + title: 'Folder Path', + type: 'short-input', + placeholder: 'Optional folder path (e.g., /Documents/Subfolder)', + condition: { field: 'operation', value: 'sharepoint_upload_file' }, + mode: 'advanced', + required: false, + }, + { + id: 'fileName', + title: 'File Name', + type: 'short-input', + placeholder: 'Optional: override uploaded file name', + condition: { field: 'operation', value: 'sharepoint_upload_file' }, + mode: 'advanced', + required: false, + }, + { + id: 'uploadFiles', + title: 'Files', + type: 'file-upload', + canonicalParamId: 'files', + placeholder: 'Upload files to SharePoint', + condition: { field: 'operation', value: 'sharepoint_upload_file' }, + mode: 'basic', + multiple: true, + required: true, + }, + { + id: 'fileRefs', + title: 'Files', + type: 'short-input', + canonicalParamId: 'files', + placeholder: 'Reference files from previous blocks', + condition: { field: 'operation', value: 'sharepoint_upload_file' }, + mode: 'advanced', + required: true, + }, + { + id: 'nextPageUrl', + title: 'Next Page URL', + type: 'short-input', + placeholder: 'Paste the @odata.nextLink URL from a previous result', + condition: { + field: 'operation', + value: ['sharepoint_read_page', 'sharepoint_list_sites', 'sharepoint_get_list'], + }, + mode: 'advanced', + }, + ], + tools: { + access: [...SHAREPOINT_V2_TOOL_IDS], + config: { + tool: (params) => params.operation || 'sharepoint_create_page', + params: (params) => { + const { + oauthCredential, + siteId, + listId, + itemId, + includeColumns, + includeItems, + columnDefinitions, + listItemFields, + files, + maxPages, + driveId, + ...rest + } = params + + const cleanString = (value: unknown) => + value === undefined || value === null ? undefined : String(value).trim() || undefined + const coerceBoolean = (value: unknown) => { + if (typeof value === 'boolean') return value + if (typeof value === 'string') return value.toLowerCase() === 'true' + return undefined + } + const parseJsonObject = (value: unknown) => { + if (typeof value !== 'string') return value + if (!value.trim()) return undefined + try { + return JSON.parse(value) + } catch (error) { + logger.error('Failed to parse SharePoint JSON input', { + error: toError(error).message, + }) + return undefined + } + } + + const normalizedFiles = normalizeFileInput(files) + const result: Record = { + ...rest, + oauthCredential, + siteId: cleanString(siteId), + listId: cleanString(listId), + itemId: cleanString(itemId), + driveId: cleanString(driveId), + includeColumns: coerceBoolean(includeColumns), + includeItems: coerceBoolean(includeItems), + listItemFields: parseJsonObject(listItemFields), + maxPages: maxPages ? Number.parseInt(String(maxPages), 10) : undefined, + } + + if (columnDefinitions) { + result.pageContent = columnDefinitions + } + if (normalizedFiles) { + result.files = normalizedFiles + } + + return result + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + oauthCredential: { type: 'string', description: 'Microsoft account credential' }, + siteId: { type: 'string', description: 'SharePoint site ID' }, + pageName: { type: 'string', description: 'Page name' }, + pageTitle: { type: 'string', description: 'Page title' }, + pageContent: { type: 'string', description: 'Page text content' }, + pageId: { type: 'string', description: 'Page ID' }, + maxPages: { type: 'number', description: 'Maximum pages to return' }, + nextPageUrl: { type: 'string', description: 'Microsoft Graph @odata.nextLink URL' }, + groupId: { type: 'string', description: 'Microsoft 365 group ID' }, + listId: { type: 'string', description: 'List ID' }, + includeColumns: { type: 'boolean', description: 'Include columns in response' }, + includeItems: { type: 'boolean', description: 'Include items in response' }, + listDisplayName: { type: 'string', description: 'List display name' }, + listDescription: { type: 'string', description: 'List description' }, + listTemplate: { type: 'string', description: 'List template' }, + columnDefinitions: { + type: 'json', + description: 'Column definitions for list creation (JSON array)', + }, + itemId: { type: 'string', description: 'List item ID' }, + listItemFields: { type: 'json', description: 'List item fields' }, + driveId: { type: 'string', description: 'Document library (drive) ID' }, + folderPath: { type: 'string', description: 'Folder path for file upload' }, + fileName: { type: 'string', description: 'File name override' }, + files: { type: 'json', description: 'Files to upload' }, + }, + outputs: { + site: { + type: 'json', + description: 'SharePoint site object (id, name, displayName, webUrl, description)', + }, + sites: { + type: 'json', + description: 'Array of SharePoint site objects (id, name, displayName, webUrl)', + }, + page: { + type: 'json', + description: 'SharePoint page object (id, name, title, webUrl, pageLayout, description)', + }, + pages: { + type: 'json', + description: 'Array of SharePoint pages with page metadata and extracted content', + }, + content: { + type: 'json', + description: 'SharePoint page content (content, canvasLayout)', + }, + totalPages: { type: 'number', description: 'Number of pages returned' }, + list: { + type: 'json', + description: 'SharePoint list object (id, displayName, name, webUrl, columns, items)', + }, + lists: { + type: 'json', + description: 'Array of SharePoint lists (id, displayName, name, webUrl, list)', + }, + item: { type: 'json', description: 'SharePoint list item with fields' }, + items: { type: 'json', description: 'Array of SharePoint list items with fields' }, + uploadedFiles: { + type: 'json', + description: 'Array of uploaded file objects with id, name, webUrl, size', + }, + fileCount: { type: 'number', description: 'Number of files uploaded' }, + skippedFiles: { + type: 'json', + description: 'Array of skipped upload files (name, size, limit, reason)', + }, + skippedCount: { type: 'number', description: 'Number of files skipped' }, + errors: { + type: 'json', + description: 'Array of per-file upload errors (name, error, status)', + }, + nextPageUrl: { + type: 'string', + description: 'Microsoft Graph @odata.nextLink URL for the next page of results', + }, + success: { type: 'boolean', description: 'Success status' }, + error: { type: 'string', description: 'Error message' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index aacf6d49431..54b2312c086 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -48,6 +48,7 @@ import { DuckDuckGoBlock } from '@/blocks/blocks/duckduckgo' import { DynamoDBBlock } from '@/blocks/blocks/dynamodb' import { ElasticsearchBlock } from '@/blocks/blocks/elasticsearch' import { ElevenLabsBlock } from '@/blocks/blocks/elevenlabs' +import { EmailBisonBlock } from '@/blocks/blocks/emailbison' import { EnrichBlock } from '@/blocks/blocks/enrich' import { EvaluatorBlock } from '@/blocks/blocks/evaluator' import { EvernoteBlock } from '@/blocks/blocks/evernote' @@ -180,7 +181,7 @@ import { SerperBlock } from '@/blocks/blocks/serper' import { ServiceNowBlock } from '@/blocks/blocks/servicenow' import { SESBlock } from '@/blocks/blocks/ses' import { SftpBlock } from '@/blocks/blocks/sftp' -import { SharepointBlock } from '@/blocks/blocks/sharepoint' +import { SharepointBlock, SharepointV2Block } from '@/blocks/blocks/sharepoint' import { ShopifyBlock } from '@/blocks/blocks/shopify' import { SimilarwebBlock } from '@/blocks/blocks/similarweb' import { SixtyfourBlock } from '@/blocks/blocks/sixtyfour' @@ -283,6 +284,7 @@ export const registry: Record = { dub: DubBlock, duckduckgo: DuckDuckGoBlock, dynamodb: DynamoDBBlock, + emailbison: EmailBisonBlock, elasticsearch: ElasticsearchBlock, elevenlabs: ElevenLabsBlock, fathom: FathomBlock, @@ -431,6 +433,7 @@ export const registry: Record = { servicenow: ServiceNowBlock, sftp: SftpBlock, sharepoint: SharepointBlock, + sharepoint_v2: SharepointV2Block, shopify: ShopifyBlock, similarweb: SimilarwebBlock, sixtyfour: SixtyfourBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index dae53828ccb..c4bc260742b 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -415,6 +415,17 @@ export function MailIcon(props: SVGProps) { ) } +export function EmailBisonIcon(props: SVGProps) { + return ( + + + + ) +} + export function MailServerIcon(props: SVGProps) { return ( { + const triggerId = providerConfig.triggerId as string | undefined + if (!triggerId) return true + + if (!isRecord(body)) { + logger.warn(`[${requestId}] Email Bison webhook payload was not an object`) + return false + } + + const { isEmailBisonEventMatch } = await import('@/triggers/emailbison/utils') + if (!isEmailBisonEventMatch(triggerId, unwrapEmailBisonPayload(body))) { + logger.info(`[${requestId}] Email Bison event did not match trigger`, { + triggerId, + }) + return false + } + + return true + }, + + async formatInput({ body, webhook }: FormatInputContext): Promise { + const payload = isRecord(body) ? unwrapEmailBisonPayload(body) : {} + const event = isRecord(payload.event) ? payload.event : null + const data = isRecord(payload.data) ? payload.data : null + const providerConfig = getProviderConfig(webhook) + const triggerId = providerConfig.triggerId as string | undefined + const input: Record = { + eventType: toStringOrNull(event?.type), + eventName: toStringOrNull(event?.name), + instanceUrl: toStringOrNull(event?.instance_url), + workspaceId: toNumberOrNull(event?.workspace_id), + workspaceName: toStringOrNull(event?.workspace_name), + event, + data, + } + + if ( + triggerId === 'emailbison_email_sent' || + triggerId === 'emailbison_lead_first_contacted' || + triggerId === 'emailbison_lead_unsubscribed' || + triggerId === 'emailbison_email_opened' + ) { + input.scheduledEmail = toRecordOrNull(data?.scheduled_email) + input.campaignEvent = renameTypeField(data?.campaign_event, 'event_type') + input.lead = toRecordOrNull(data?.lead) + input.campaign = toRecordOrNull(data?.campaign) + input.senderEmail = renameTypeField(data?.sender_email, 'account_type') + } + + if ( + triggerId === 'emailbison_lead_replied' || + triggerId === 'emailbison_lead_interested' || + triggerId === 'emailbison_email_bounced' + ) { + input.reply = renameTypeField(data?.reply, 'reply_type') + input.campaignEvent = renameTypeField(data?.campaign_event, 'event_type') + input.lead = toRecordOrNull(data?.lead) + input.campaign = toRecordOrNull(data?.campaign) + input.scheduledEmail = toRecordOrNull(data?.scheduled_email) + input.senderEmail = renameTypeField(data?.sender_email, 'account_type') + } + + if (triggerId === 'emailbison_untracked_reply_received') { + input.reply = renameTypeField(data?.reply, 'reply_type') + input.senderEmail = renameTypeField(data?.sender_email, 'account_type') + } + + if ( + triggerId === 'emailbison_email_account_added' || + triggerId === 'emailbison_email_account_removed' || + triggerId === 'emailbison_email_account_disconnected' || + triggerId === 'emailbison_email_account_reconnected' || + triggerId === 'emailbison_warmup_disabled_receiving_bounces' || + triggerId === 'emailbison_warmup_disabled_causing_bounces' + ) { + input.senderEmail = renameTypeField(data?.sender_email, 'account_type') + } + + if (triggerId === 'emailbison_manual_email_sent') { + input.reply = renameTypeField(data?.reply, 'reply_type') + input.lead = toRecordOrNull(data?.lead) + input.campaign = toRecordOrNull(data?.campaign) + input.scheduledEmail = toRecordOrNull(data?.scheduled_email) + input.senderEmail = renameTypeField(data?.sender_email, 'account_type') + } + + if (triggerId === 'emailbison_tag_attached' || triggerId === 'emailbison_tag_removed') { + input.tagId = toNumberOrNull(data?.tag_id) + input.tagName = toStringOrNull(data?.tag_name) + input.taggableId = toNumberOrNull(data?.taggable_id) + input.taggableType = toStringOrNull(data?.taggable_type) + } + + return { + input, + } + }, + + async createSubscription(ctx: SubscriptionContext): Promise { + const { webhook, requestId } = ctx + const providerConfig = getProviderConfig(webhook) + const apiKey = providerConfig.apiKey as string | undefined + const apiBaseUrl = providerConfig.apiBaseUrl as string | undefined + const triggerId = providerConfig.triggerId as string | undefined + + if (!apiKey?.trim()) { + throw new Error('Email Bison API Key is required.') + } + + if (!apiBaseUrl?.trim()) { + throw new Error('Email Bison Instance URL is required.') + } + + if (!triggerId) { + throw new Error('Email Bison trigger ID is required.') + } + + const { getEmailBisonEventTypeForTrigger } = await import('@/triggers/emailbison/utils') + const eventType = getEmailBisonEventTypeForTrigger(triggerId) + if (!eventType) { + throw new Error(`Unknown Email Bison trigger type: ${triggerId}`) + } + + const notificationUrl = getNotificationUrl(webhook) + + logger.info(`[${requestId}] Creating Email Bison webhook`, { + triggerId, + eventType, + webhookId: webhook.id, + }) + + const response = await fetch(emailBisonUrl('/api/webhook-url', {}, apiBaseUrl), { + method: 'POST', + headers: emailBisonHeaders({ apiKey, apiBaseUrl }), + body: JSON.stringify({ + name: `Sim - ${eventType}`, + url: notificationUrl, + events: [eventType], + }), + }) + + const responseBody = await parseJsonResponse(response) + if (!response.ok) { + const message = extractEmailBisonError(responseBody) + logger.error(`[${requestId}] Failed to create Email Bison webhook`, { + status: response.status, + message, + response: responseBody, + }) + + if (response.status === 401 || response.status === 403) { + throw new Error( + 'Invalid Email Bison API Key or Instance URL. Confirm both came from the same Email Bison instance.' + ) + } + + throw new Error( + message ? `Email Bison error: ${message}` : 'Failed to create Email Bison webhook' + ) + } + + const data = isRecord(responseBody?.data) ? responseBody.data : null + const externalId = data?.id + if (externalId === undefined || externalId === null || externalId === '') { + throw new Error('Email Bison webhook was created but the API response did not include an ID.') + } + + logger.info(`[${requestId}] Successfully created Email Bison webhook`, { + externalId, + webhookId: webhook.id, + }) + + return { providerConfigUpdates: { externalId: String(externalId) } } + }, + + async deleteSubscription(ctx: DeleteSubscriptionContext): Promise { + const { webhook, requestId } = ctx + + try { + const providerConfig = getProviderConfig(webhook) + const apiKey = providerConfig.apiKey as string | undefined + const apiBaseUrl = providerConfig.apiBaseUrl as string | undefined + const externalId = providerConfig.externalId as string | undefined + + if (!apiKey?.trim() || !apiBaseUrl?.trim() || !externalId?.trim()) { + logger.warn(`[${requestId}] Missing Email Bison webhook cleanup configuration`, { + webhookId: webhook.id, + hasApiKey: Boolean(apiKey), + hasApiBaseUrl: Boolean(apiBaseUrl), + hasExternalId: Boolean(externalId), + }) + return + } + + const response = await fetch( + emailBisonUrl(`/api/webhook-url/${encodeURIComponent(externalId)}`, {}, apiBaseUrl), + { + method: 'DELETE', + headers: emailBisonHeaders({ apiKey, apiBaseUrl }), + } + ) + + if (!response.ok && response.status !== 404) { + const responseBody = await parseJsonResponse(response) + logger.warn(`[${requestId}] Failed to delete Email Bison webhook`, { + status: response.status, + response: responseBody, + }) + return + } + + await response.body?.cancel() + logger.info(`[${requestId}] Successfully deleted Email Bison webhook`, { + externalId, + webhookId: webhook.id, + }) + } catch (error) { + logger.warn(`[${requestId}] Error deleting Email Bison webhook`, { + message: toError(error).message, + }) + } + }, +} + +async function parseJsonResponse(response: Response): Promise | null> { + try { + const body: unknown = await response.json() + return isRecord(body) ? body : null + } catch { + return null + } +} + +function extractEmailBisonError(body: Record | null): string | null { + if (!body) return null + if (typeof body.message === 'string') return body.message + if (typeof body.error === 'string') return body.error + + const data = body.data + if (isRecord(data) && typeof data.message === 'string') return data.message + + return null +} + +function unwrapEmailBisonPayload(body: Record): Record { + if (isRecord(body.event)) return body + + const data = body.data + if (isRecord(data) && isRecord(data.event)) return data + + const payload = isRecord(data) ? data.payload : body.payload + if (isRecord(payload) && isRecord(payload.event)) return payload + + return body +} + +function toStringOrNull(value: unknown): string | null { + if (value === undefined || value === null) return null + return String(value) +} + +function toNumberOrNull(value: unknown): number | null { + if (typeof value === 'number') return Number.isFinite(value) ? value : null + if (typeof value !== 'string' || value.trim() === '') return null + + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : null +} + +function toRecordOrNull(value: unknown): Record | null { + return isRecord(value) ? value : null +} + +function renameTypeField(value: unknown, targetKey: string): Record | null { + if (!isRecord(value)) return null + + const { type, ...rest } = value + return { ...rest, [targetKey]: type ?? null } +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index c20b58f3f02..ce8e9c6af73 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -7,6 +7,7 @@ import { calcomHandler } from '@/lib/webhooks/providers/calcom' import { calendlyHandler } from '@/lib/webhooks/providers/calendly' import { circlebackHandler } from '@/lib/webhooks/providers/circleback' import { confluenceHandler } from '@/lib/webhooks/providers/confluence' +import { emailBisonHandler } from '@/lib/webhooks/providers/emailbison' import { fathomHandler } from '@/lib/webhooks/providers/fathom' import { firefliesHandler } from '@/lib/webhooks/providers/fireflies' import { genericHandler } from '@/lib/webhooks/providers/generic' @@ -55,6 +56,7 @@ const PROVIDER_HANDLERS: Record = { calcom: calcomHandler, circleback: circlebackHandler, confluence: confluenceHandler, + emailbison: emailBisonHandler, fireflies: firefliesHandler, generic: genericHandler, gmail: gmailHandler, diff --git a/apps/sim/tools/emailbison/attach_leads_to_campaign.ts b/apps/sim/tools/emailbison/attach_leads_to_campaign.ts new file mode 100644 index 00000000000..79a9a68d5d8 --- /dev/null +++ b/apps/sim/tools/emailbison/attach_leads_to_campaign.ts @@ -0,0 +1,70 @@ +import type { + EmailBisonActionResponse, + EmailBisonAttachLeadsParams, +} from '@/tools/emailbison/types' +import { + actionOutput, + actionOutputs, + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonRecordData, + emailBisonUrl, + jsonBody, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +export const attachLeadsToCampaignTool: ToolConfig< + EmailBisonAttachLeadsParams, + EmailBisonActionResponse +> = { + id: 'emailbison_attach_leads_to_campaign', + name: 'Email Bison Attach Leads to Campaign', + description: 'Adds existing Email Bison leads to a campaign.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + campaignId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Campaign ID', + }, + leadIds: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Lead IDs to add to the campaign', + items: { type: 'number', description: 'Lead ID' }, + }, + allowParallelSending: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Force add leads already in sequence in other campaigns', + }, + }, + request: { + url: (params) => + emailBisonUrl( + `/api/campaigns/${params.campaignId}/leads/attach-leads`, + {}, + params.apiBaseUrl + ), + method: 'POST', + headers: emailBisonHeaders, + body: (params) => + jsonBody({ + lead_ids: params.leadIds, + allow_parallel_sending: params.allowParallelSending, + }), + }, + transformResponse: async (response) => { + const data = await emailBisonRecordData(response, 'campaign lead attachment result') + + return { + success: true, + output: actionOutput(data), + } + }, + outputs: actionOutputs, +} diff --git a/apps/sim/tools/emailbison/attach_tags_to_leads.ts b/apps/sim/tools/emailbison/attach_tags_to_leads.ts new file mode 100644 index 00000000000..ff78161b6d5 --- /dev/null +++ b/apps/sim/tools/emailbison/attach_tags_to_leads.ts @@ -0,0 +1,67 @@ +import type { + EmailBisonActionResponse, + EmailBisonAttachTagsToLeadsParams, +} from '@/tools/emailbison/types' +import { + actionOutput, + actionOutputs, + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonRecordData, + emailBisonUrl, + jsonBody, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +export const attachTagsToLeadsTool: ToolConfig< + EmailBisonAttachTagsToLeadsParams, + EmailBisonActionResponse +> = { + id: 'emailbison_attach_tags_to_leads', + name: 'Email Bison Attach Tags to Leads', + description: 'Attaches Email Bison tags to one or more leads.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + tagIds: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Tag IDs to attach', + items: { type: 'number', description: 'Tag ID' }, + }, + leadIds: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Lead IDs to tag', + items: { type: 'number', description: 'Lead ID' }, + }, + skipWebhooks: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Skip Email Bison webhooks for this action', + }, + }, + request: { + url: (params) => emailBisonUrl('/api/tags/attach-to-leads', {}, params.apiBaseUrl), + method: 'POST', + headers: emailBisonHeaders, + body: (params) => + jsonBody({ + tag_ids: params.tagIds, + lead_ids: params.leadIds, + skip_webhooks: params.skipWebhooks, + }), + }, + transformResponse: async (response) => { + const data = await emailBisonRecordData(response, 'tag attachment result') + + return { + success: true, + output: actionOutput(data), + } + }, + outputs: actionOutputs, +} diff --git a/apps/sim/tools/emailbison/create_campaign.ts b/apps/sim/tools/emailbison/create_campaign.ts new file mode 100644 index 00000000000..b264cbdf7d0 --- /dev/null +++ b/apps/sim/tools/emailbison/create_campaign.ts @@ -0,0 +1,58 @@ +import type { + EmailBisonCampaignResponse, + EmailBisonCreateCampaignParams, +} from '@/tools/emailbison/types' +import { + campaignOutputs, + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonRecordData, + emailBisonUrl, + jsonBody, + mapCampaign, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +export const createCampaignTool: ToolConfig< + EmailBisonCreateCampaignParams, + EmailBisonCampaignResponse +> = { + id: 'emailbison_create_campaign', + name: 'Email Bison Create Campaign', + description: 'Creates a new Email Bison campaign.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Campaign name', + }, + campaignType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Campaign type: outbound or reply_followup', + }, + }, + request: { + url: (params) => emailBisonUrl('/api/campaigns', {}, params.apiBaseUrl), + method: 'POST', + headers: emailBisonHeaders, + body: (params) => + jsonBody({ + name: params.name, + type: params.campaignType, + }), + }, + transformResponse: async (response) => { + const data = await emailBisonRecordData(response, 'campaign') + + return { + success: true, + output: mapCampaign(data), + } + }, + outputs: campaignOutputs, +} diff --git a/apps/sim/tools/emailbison/create_lead.ts b/apps/sim/tools/emailbison/create_lead.ts new file mode 100644 index 00000000000..58d4051a7f3 --- /dev/null +++ b/apps/sim/tools/emailbison/create_lead.ts @@ -0,0 +1,94 @@ +import type { EmailBisonLeadMutationParams, EmailBisonLeadResponse } from '@/tools/emailbison/types' +import { + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonRecordData, + emailBisonUrl, + jsonBody, + leadOutputs, + mapLead, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +export const createLeadTool: ToolConfig = { + id: 'emailbison_create_lead', + name: 'Email Bison Create Lead', + description: 'Creates a single lead in Email Bison.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + firstName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Lead first name', + }, + lastName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Lead last name', + }, + email: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Lead email address', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lead job title', + }, + company: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lead company', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Additional notes about the lead', + }, + customVariables: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Custom variables to store on the lead', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Custom variable name' }, + value: { type: 'string', description: 'Custom variable value' }, + }, + }, + }, + }, + request: { + url: (params) => emailBisonUrl('/api/leads', {}, params.apiBaseUrl), + method: 'POST', + headers: emailBisonHeaders, + body: (params) => + jsonBody({ + first_name: params.firstName, + last_name: params.lastName, + email: params.email, + title: params.title, + company: params.company, + notes: params.notes, + custom_variables: params.customVariables, + }), + }, + transformResponse: async (response) => { + const data = await emailBisonRecordData(response, 'lead') + + return { + success: true, + output: mapLead(data), + } + }, + outputs: leadOutputs, +} diff --git a/apps/sim/tools/emailbison/create_tag.ts b/apps/sim/tools/emailbison/create_tag.ts new file mode 100644 index 00000000000..7086dd699ff --- /dev/null +++ b/apps/sim/tools/emailbison/create_tag.ts @@ -0,0 +1,42 @@ +import type { EmailBisonCreateTagParams, EmailBisonTagResponse } from '@/tools/emailbison/types' +import { + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonRecordData, + emailBisonUrl, + jsonBody, + mapTag, + tagOutputs, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +export const createTagTool: ToolConfig = { + id: 'emailbison_create_tag', + name: 'Email Bison Create Tag', + description: 'Creates a new Email Bison tag.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tag name', + }, + }, + request: { + url: (params) => emailBisonUrl('/api/tags', {}, params.apiBaseUrl), + method: 'POST', + headers: emailBisonHeaders, + body: (params) => jsonBody({ name: params.name }), + }, + transformResponse: async (response) => { + const data = await emailBisonRecordData(response, 'tag') + + return { + success: true, + output: mapTag(data), + } + }, + outputs: tagOutputs, +} diff --git a/apps/sim/tools/emailbison/get_lead.ts b/apps/sim/tools/emailbison/get_lead.ts new file mode 100644 index 00000000000..02353615592 --- /dev/null +++ b/apps/sim/tools/emailbison/get_lead.ts @@ -0,0 +1,45 @@ +import type { EmailBisonGetLeadParams, EmailBisonLeadResponse } from '@/tools/emailbison/types' +import { + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonRecordData, + emailBisonUrl, + leadOutputs, + mapLead, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +export const getLeadTool: ToolConfig = { + id: 'emailbison_get_lead', + name: 'Email Bison Get Lead', + description: 'Retrieves a lead by Email Bison lead ID or email address.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + leadId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Lead ID or email address', + }, + }, + request: { + url: (params) => + emailBisonUrl( + `/api/leads/${encodeURIComponent(params.leadId.trim())}`, + {}, + params.apiBaseUrl + ), + method: 'GET', + headers: emailBisonHeaders, + }, + transformResponse: async (response) => { + const data = await emailBisonRecordData(response, 'lead') + + return { + success: true, + output: mapLead(data), + } + }, + outputs: leadOutputs, +} diff --git a/apps/sim/tools/emailbison/index.ts b/apps/sim/tools/emailbison/index.ts new file mode 100644 index 00000000000..ecbcfb6aa2a --- /dev/null +++ b/apps/sim/tools/emailbison/index.ts @@ -0,0 +1,14 @@ +export { attachLeadsToCampaignTool as emailBisonAttachLeadsToCampaignTool } from '@/tools/emailbison/attach_leads_to_campaign' +export { attachTagsToLeadsTool as emailBisonAttachTagsToLeadsTool } from '@/tools/emailbison/attach_tags_to_leads' +export { createCampaignTool as emailBisonCreateCampaignTool } from '@/tools/emailbison/create_campaign' +export { createLeadTool as emailBisonCreateLeadTool } from '@/tools/emailbison/create_lead' +export { createTagTool as emailBisonCreateTagTool } from '@/tools/emailbison/create_tag' +export { getLeadTool as emailBisonGetLeadTool } from '@/tools/emailbison/get_lead' +export { listCampaignsTool as emailBisonListCampaignsTool } from '@/tools/emailbison/list_campaigns' +export { listLeadsTool as emailBisonListLeadsTool } from '@/tools/emailbison/list_leads' +export { listRepliesTool as emailBisonListRepliesTool } from '@/tools/emailbison/list_replies' +export { listTagsTool as emailBisonListTagsTool } from '@/tools/emailbison/list_tags' +export * from '@/tools/emailbison/types' +export { updateCampaignTool as emailBisonUpdateCampaignTool } from '@/tools/emailbison/update_campaign' +export { updateCampaignStatusTool as emailBisonUpdateCampaignStatusTool } from '@/tools/emailbison/update_campaign_status' +export { updateLeadTool as emailBisonUpdateLeadTool } from '@/tools/emailbison/update_lead' diff --git a/apps/sim/tools/emailbison/list_campaigns.ts b/apps/sim/tools/emailbison/list_campaigns.ts new file mode 100644 index 00000000000..3987ac6116e --- /dev/null +++ b/apps/sim/tools/emailbison/list_campaigns.ts @@ -0,0 +1,42 @@ +import type { + EmailBisonBaseParams, + EmailBisonListCampaignsResponse, +} from '@/tools/emailbison/types' +import { + emailBisonArrayData, + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonUrl, + listCampaignsOutputs, + mapCampaign, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +export const listCampaignsTool: ToolConfig = + { + id: 'emailbison_list_campaigns', + name: 'Email Bison List Campaigns', + description: 'Retrieves Email Bison campaigns.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + }, + request: { + url: (params) => emailBisonUrl('/api/campaigns', {}, params.apiBaseUrl), + method: 'GET', + headers: emailBisonHeaders, + }, + transformResponse: async (response) => { + const data = await emailBisonArrayData(response, 'campaigns') + const campaigns = data.map(mapCampaign) + + return { + success: true, + output: { + campaigns, + count: campaigns.length, + }, + } + }, + outputs: listCampaignsOutputs, + } diff --git a/apps/sim/tools/emailbison/list_leads.ts b/apps/sim/tools/emailbison/list_leads.ts new file mode 100644 index 00000000000..b79b6bb0b8c --- /dev/null +++ b/apps/sim/tools/emailbison/list_leads.ts @@ -0,0 +1,85 @@ +import type { + EmailBisonListLeadsParams, + EmailBisonListLeadsResponse, +} from '@/tools/emailbison/types' +import { + emailBisonArrayData, + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonUrl, + listLeadsOutputs, + mapLead, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +export const listLeadsTool: ToolConfig = { + id: 'emailbison_list_leads', + name: 'Email Bison List Leads', + description: 'Retrieves leads from Email Bison with optional search and tag filters.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search term for filtering leads', + }, + campaignStatus: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Lead campaign status filter: in_sequence, sequence_finished, sequence_stopped, never_contacted, or replied', + }, + tagIds: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Tag IDs to include', + items: { type: 'number', description: 'Tag ID' }, + }, + excludedTagIds: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Tag IDs to exclude', + items: { type: 'number', description: 'Tag ID' }, + }, + withoutTags: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Only return leads without tags', + }, + }, + request: { + url: (params) => + emailBisonUrl( + '/api/leads', + { + search: params.search, + 'filters.lead_campaign_status': params.campaignStatus, + 'filters.tag_ids': params.tagIds, + 'filters.excluded_tag_ids': params.excludedTagIds, + 'filters.without_tags': params.withoutTags, + }, + params.apiBaseUrl + ), + method: 'GET', + headers: emailBisonHeaders, + }, + transformResponse: async (response) => { + const data = await emailBisonArrayData(response, 'leads') + const leads = data.map(mapLead) + + return { + success: true, + output: { + leads, + count: leads.length, + }, + } + }, + outputs: listLeadsOutputs, +} diff --git a/apps/sim/tools/emailbison/list_replies.ts b/apps/sim/tools/emailbison/list_replies.ts new file mode 100644 index 00000000000..97f3771832f --- /dev/null +++ b/apps/sim/tools/emailbison/list_replies.ts @@ -0,0 +1,108 @@ +import type { + EmailBisonListRepliesParams, + EmailBisonListRepliesResponse, +} from '@/tools/emailbison/types' +import { + emailBisonArrayData, + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonUrl, + listRepliesOutputs, + mapReply, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +export const listRepliesTool: ToolConfig< + EmailBisonListRepliesParams, + EmailBisonListRepliesResponse +> = { + id: 'emailbison_list_replies', + name: 'Email Bison List Replies', + description: + 'Retrieves Email Bison replies with optional status, folder, campaign, sender, lead, and tag filters.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search term for replies', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reply status: interested, automated_reply, or not_automated_reply', + }, + folder: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reply folder: inbox, sent, spam, bounced, or all', + }, + read: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Filter by read state', + }, + campaignId: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Campaign ID', + }, + senderEmailId: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Sender email ID', + }, + leadId: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Lead ID', + }, + tagIds: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Tag IDs to filter replies by', + items: { type: 'number', description: 'Tag ID' }, + }, + }, + request: { + url: (params) => + emailBisonUrl( + '/api/replies', + { + search: params.search, + status: params.status, + folder: params.folder, + read: params.read, + campaign_id: params.campaignId, + sender_email_id: params.senderEmailId, + lead_id: params.leadId, + tag_ids: params.tagIds, + }, + params.apiBaseUrl + ), + method: 'GET', + headers: emailBisonHeaders, + }, + transformResponse: async (response) => { + const data = await emailBisonArrayData(response, 'replies') + const replies = data.map(mapReply) + + return { + success: true, + output: { + replies, + count: replies.length, + }, + } + }, + outputs: listRepliesOutputs, +} diff --git a/apps/sim/tools/emailbison/list_tags.ts b/apps/sim/tools/emailbison/list_tags.ts new file mode 100644 index 00000000000..f0811da7737 --- /dev/null +++ b/apps/sim/tools/emailbison/list_tags.ts @@ -0,0 +1,38 @@ +import type { EmailBisonBaseParams, EmailBisonListTagsResponse } from '@/tools/emailbison/types' +import { + emailBisonArrayData, + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonUrl, + listTagsOutputs, + mapTag, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +export const listTagsTool: ToolConfig = { + id: 'emailbison_list_tags', + name: 'Email Bison List Tags', + description: 'Retrieves all Email Bison tags for the authenticated workspace.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + }, + request: { + url: (params) => emailBisonUrl('/api/tags', {}, params.apiBaseUrl), + method: 'GET', + headers: emailBisonHeaders, + }, + transformResponse: async (response) => { + const data = await emailBisonArrayData(response, 'tags') + const tags = data.map(mapTag) + + return { + success: true, + output: { + tags, + count: tags.length, + }, + } + }, + outputs: listTagsOutputs, +} diff --git a/apps/sim/tools/emailbison/types.ts b/apps/sim/tools/emailbison/types.ts new file mode 100644 index 00000000000..37977309c6d --- /dev/null +++ b/apps/sim/tools/emailbison/types.ts @@ -0,0 +1,254 @@ +import type { ToolResponse } from '@/tools/types' + +export interface EmailBisonBaseParams { + apiKey: string + apiBaseUrl: string +} + +export interface EmailBisonCustomVariable { + name: string | null + value: string | null +} + +export interface EmailBisonLeadStats { + emails_sent: number | null + opens: number | null + replies: number | null + unique_replies: number | null + unique_opens: number | null +} + +export interface EmailBisonLead { + id: number | null + first_name: string | null + last_name: string | null + email: string | null + title: string | null + company: string | null + notes: string | null + status: string | null + custom_variables: EmailBisonCustomVariable[] + lead_campaign_data: unknown[] + overall_stats: EmailBisonLeadStats + created_at: string | null + updated_at: string | null +} + +export interface EmailBisonTag { + id: number | null + name: string | null + default: boolean | null + created_at?: string | null + updated_at?: string | null +} + +export interface EmailBisonCampaignTag { + id: number | null + name: string | null + default: boolean | null +} + +export interface EmailBisonCampaign { + id: number | null + uuid: string | null + name: string | null + type: string | null + status: string | null + emails_sent: number | null + opened: number | null + unique_opens: number | null + replied: number | null + unique_replies: number | null + bounced: number | null + unsubscribed: number | null + interested: number | null + total_leads_contacted: number | null + total_leads: number | null + max_emails_per_day: number | null + max_new_leads_per_day: number | null + plain_text: boolean | null + open_tracking: boolean | null + can_unsubscribe: boolean | null + unsubscribe_text: string | null + sequence_prioritization?: string | null + tags: EmailBisonCampaignTag[] + created_at: string | null + updated_at: string | null +} + +export interface EmailBisonReplyAddress { + name: string | null + address: string | null +} + +export interface EmailBisonReplyAttachment { + id: number | null + uuid: string | null + reply_id: number | null + file_name: string | null + download_url: string | null + created_at: string | null + updated_at: string | null +} + +export interface EmailBisonReply { + id: number | null + uuid: string | null + folder: string | null + subject: string | null + read: boolean | null + interested: boolean | null + automated_reply: boolean | null + html_body: string | null + text_body: string | null + raw_body: string | null + headers: string | null + date_received: string | null + type: string | null + tracked_reply: boolean | null + scheduled_email_id: number | string | null + campaign_id: number | string | null + lead_id: number | null + sender_email_id: number | null + raw_message_id: string | null + from_name: string | null + from_email_address: string | null + primary_to_email_address: string | null + to: EmailBisonReplyAddress[] + cc: string | null + bcc: string | null + parent_id: number | string | null + attachments: EmailBisonReplyAttachment[] + created_at: string | null + updated_at: string | null +} + +export interface EmailBisonListLeadsParams extends EmailBisonBaseParams { + search?: string + campaignStatus?: string + tagIds?: number[] + excludedTagIds?: number[] + withoutTags?: boolean +} + +export interface EmailBisonGetLeadParams extends EmailBisonBaseParams { + leadId: string +} + +export interface EmailBisonLeadMutationParams extends EmailBisonBaseParams { + leadId?: string + firstName: string + lastName: string + email: string + title?: string + company?: string + notes?: string + customVariables?: EmailBisonCustomVariable[] +} + +export interface EmailBisonCreateCampaignParams extends EmailBisonBaseParams { + name: string + campaignType?: 'outbound' | 'reply_followup' +} + +export interface EmailBisonUpdateCampaignParams extends EmailBisonBaseParams { + campaignId: number + name?: string + maxEmailsPerDay?: number + maxNewLeadsPerDay?: number + plainText?: boolean + openTracking?: boolean + reputationBuilding?: boolean + canUnsubscribe?: boolean + includeAutoRepliesInStats?: boolean + sequencePrioritization?: 'followups' | 'new_leads' +} + +export interface EmailBisonCampaignStatusParams extends EmailBisonBaseParams { + campaignId: number + action: 'pause' | 'resume' | 'archive' +} + +export interface EmailBisonAttachLeadsParams extends EmailBisonBaseParams { + campaignId: number + leadIds: number[] + allowParallelSending?: boolean +} + +export interface EmailBisonListRepliesParams extends EmailBisonBaseParams { + search?: string + status?: string + folder?: string + read?: boolean + campaignId?: number + senderEmailId?: number + leadId?: number + tagIds?: number[] +} + +export interface EmailBisonCreateTagParams extends EmailBisonBaseParams { + name: string +} + +export interface EmailBisonAttachTagsToLeadsParams extends EmailBisonBaseParams { + tagIds: number[] + leadIds: number[] + skipWebhooks?: boolean +} + +export interface EmailBisonListLeadsResponse extends ToolResponse { + output: { + leads: EmailBisonLead[] + count: number + } +} + +export interface EmailBisonLeadResponse extends ToolResponse { + output: EmailBisonLead +} + +export interface EmailBisonListCampaignsResponse extends ToolResponse { + output: { + campaigns: EmailBisonCampaign[] + count: number + } +} + +export interface EmailBisonCampaignResponse extends ToolResponse { + output: EmailBisonCampaign +} + +export interface EmailBisonActionResponse extends ToolResponse { + output: { + success: boolean + message: string | null + } +} + +export interface EmailBisonListRepliesResponse extends ToolResponse { + output: { + replies: EmailBisonReply[] + count: number + } +} + +export interface EmailBisonListTagsResponse extends ToolResponse { + output: { + tags: EmailBisonTag[] + count: number + } +} + +export interface EmailBisonTagResponse extends ToolResponse { + output: EmailBisonTag +} + +export type EmailBisonResponse = + | EmailBisonListLeadsResponse + | EmailBisonLeadResponse + | EmailBisonListCampaignsResponse + | EmailBisonCampaignResponse + | EmailBisonActionResponse + | EmailBisonListRepliesResponse + | EmailBisonListTagsResponse + | EmailBisonTagResponse diff --git a/apps/sim/tools/emailbison/update_campaign.ts b/apps/sim/tools/emailbison/update_campaign.ts new file mode 100644 index 00000000000..2ff767b9001 --- /dev/null +++ b/apps/sim/tools/emailbison/update_campaign.ts @@ -0,0 +1,114 @@ +import type { + EmailBisonCampaignResponse, + EmailBisonUpdateCampaignParams, +} from '@/tools/emailbison/types' +import { + campaignOutputs, + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonRecordData, + emailBisonUrl, + jsonBody, + mapCampaign, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateCampaignTool: ToolConfig< + EmailBisonUpdateCampaignParams, + EmailBisonCampaignResponse +> = { + id: 'emailbison_update_campaign', + name: 'Email Bison Update Campaign', + description: 'Updates Email Bison campaign settings.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + campaignId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Campaign ID', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Campaign name', + }, + maxEmailsPerDay: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum emails per day', + }, + maxNewLeadsPerDay: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum new leads per day', + }, + plainText: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Send plain text emails', + }, + openTracking: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Enable open tracking', + }, + reputationBuilding: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Enable reputation building', + }, + canUnsubscribe: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Enable unsubscribe link', + }, + includeAutoRepliesInStats: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include auto replies in campaign stats', + }, + sequencePrioritization: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sequence prioritization: followups or new_leads', + }, + }, + request: { + url: (params) => + emailBisonUrl(`/api/campaigns/${params.campaignId}/update`, {}, params.apiBaseUrl), + method: 'PATCH', + headers: emailBisonHeaders, + body: (params) => + jsonBody({ + name: params.name, + max_emails_per_day: params.maxEmailsPerDay, + max_new_leads_per_day: params.maxNewLeadsPerDay, + plain_text: params.plainText, + open_tracking: params.openTracking, + reputation_building: params.reputationBuilding, + can_unsubscribe: params.canUnsubscribe, + include_auto_replies_in_stats: params.includeAutoRepliesInStats, + sequence_prioritization: params.sequencePrioritization, + }), + }, + transformResponse: async (response) => { + const data = await emailBisonRecordData(response, 'campaign') + + return { + success: true, + output: mapCampaign(data), + } + }, + outputs: campaignOutputs, +} diff --git a/apps/sim/tools/emailbison/update_campaign_status.ts b/apps/sim/tools/emailbison/update_campaign_status.ts new file mode 100644 index 00000000000..4557e47ed8e --- /dev/null +++ b/apps/sim/tools/emailbison/update_campaign_status.ts @@ -0,0 +1,64 @@ +import type { + EmailBisonCampaignResponse, + EmailBisonCampaignStatusParams, +} from '@/tools/emailbison/types' +import { + campaignOutputs, + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonRecordData, + emailBisonUrl, + mapCampaign, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +const CAMPAIGN_STATUS_ACTIONS = new Set(['pause', 'resume', 'archive']) + +export const updateCampaignStatusTool: ToolConfig< + EmailBisonCampaignStatusParams, + EmailBisonCampaignResponse +> = { + id: 'emailbison_update_campaign_status', + name: 'Email Bison Update Campaign Status', + description: 'Pauses, resumes, or archives an Email Bison campaign.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + campaignId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Campaign ID', + }, + action: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Status action: pause, resume, or archive', + }, + }, + request: { + url: (params) => { + if (!CAMPAIGN_STATUS_ACTIONS.has(params.action)) { + throw new Error('Email Bison campaign status action must be pause, resume, or archive') + } + + return emailBisonUrl( + `/api/campaigns/${params.campaignId}/${params.action}`, + {}, + params.apiBaseUrl + ) + }, + method: 'PATCH', + headers: emailBisonHeaders, + }, + transformResponse: async (response) => { + const data = await emailBisonRecordData(response, 'campaign') + + return { + success: true, + output: mapCampaign(data), + } + }, + outputs: campaignOutputs, +} diff --git a/apps/sim/tools/emailbison/update_lead.ts b/apps/sim/tools/emailbison/update_lead.ts new file mode 100644 index 00000000000..fb423d8b9b5 --- /dev/null +++ b/apps/sim/tools/emailbison/update_lead.ts @@ -0,0 +1,106 @@ +import type { EmailBisonLeadMutationParams, EmailBisonLeadResponse } from '@/tools/emailbison/types' +import { + emailBisonBaseParamFields, + emailBisonHeaders, + emailBisonRecordData, + emailBisonUrl, + jsonBody, + leadOutputs, + mapLead, +} from '@/tools/emailbison/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateLeadTool: ToolConfig = { + id: 'emailbison_update_lead', + name: 'Email Bison Update Lead', + description: + 'Updates an existing Email Bison lead. Fields omitted from a PUT update may be cleared by Email Bison.', + version: '1.0.0', + params: { + ...emailBisonBaseParamFields, + leadId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Lead ID or email address', + }, + firstName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Lead first name', + }, + lastName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Lead last name', + }, + email: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Lead email address', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lead job title', + }, + company: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lead company', + }, + notes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Additional notes about the lead', + }, + customVariables: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Custom variables to store on the lead', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Custom variable name' }, + value: { type: 'string', description: 'Custom variable value' }, + }, + }, + }, + }, + request: { + url: (params) => + emailBisonUrl( + `/api/leads/${encodeURIComponent(params.leadId?.trim() ?? '')}`, + {}, + params.apiBaseUrl + ), + method: 'PUT', + headers: emailBisonHeaders, + body: (params) => + jsonBody({ + first_name: params.firstName, + last_name: params.lastName, + email: params.email, + title: params.title, + company: params.company, + notes: params.notes, + custom_variables: params.customVariables, + }), + }, + transformResponse: async (response) => { + const data = await emailBisonRecordData(response, 'lead') + + return { + success: true, + output: mapLead(data), + } + }, + outputs: leadOutputs, +} diff --git a/apps/sim/tools/emailbison/utils.ts b/apps/sim/tools/emailbison/utils.ts new file mode 100644 index 00000000000..844d6867dca --- /dev/null +++ b/apps/sim/tools/emailbison/utils.ts @@ -0,0 +1,482 @@ +import type { + EmailBisonBaseParams, + EmailBisonCampaign, + EmailBisonCampaignTag, + EmailBisonLead, + EmailBisonLeadStats, + EmailBisonReply, + EmailBisonReplyAddress, + EmailBisonReplyAttachment, + EmailBisonTag, +} from '@/tools/emailbison/types' +import type { OutputProperty, ToolConfig } from '@/tools/types' + +type QueryValue = string | number | boolean | Array | undefined | null + +interface EmailBisonEnvelope { + data?: T +} + +export function emailBisonHeaders(params: EmailBisonBaseParams): Record { + return { + Authorization: `Bearer ${params.apiKey.trim()}`, + 'Content-Type': 'application/json', + } +} + +export const emailBisonBaseParamFields = { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Email Bison API token', + }, + apiBaseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Email Bison instance URL that issued the token', + }, +} satisfies ToolConfig['params'] + +export function emailBisonUrl( + path: string, + query: Record, + baseUrl: string +): string { + const url = new URL(path, normalizeEmailBisonBaseUrl(baseUrl)) + + Object.entries(query).forEach(([key, value]) => { + if (value === undefined || value === null || value === '') return + + if (Array.isArray(value)) { + value.forEach((item) => { + url.searchParams.append(key, String(item)) + }) + return + } + + url.searchParams.set(key, String(value)) + }) + + return url.toString() +} + +function normalizeEmailBisonBaseUrl(baseUrl: string): string { + const trimmedBaseUrl = baseUrl.trim() + if (!trimmedBaseUrl) { + throw new Error('Email Bison Instance URL is required') + } + + const rawBaseUrl = /^https?:\/\//i.test(trimmedBaseUrl) + ? trimmedBaseUrl + : `https://${trimmedBaseUrl}` + const parsed = new URL(rawBaseUrl) + + if (parsed.protocol !== 'https:') { + throw new Error('Email Bison Instance URL must use HTTPS') + } + + return parsed.origin +} + +export function jsonBody(fields: Record): Record { + return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined)) +} + +export async function emailBisonData(response: Response): Promise { + const payload = (await response.json()) as EmailBisonEnvelope + return payload.data ?? null +} + +export async function emailBisonArrayData(response: Response, label: string): Promise { + const data = await emailBisonData(response) + if (!Array.isArray(data)) { + throw new Error(`Email Bison response did not include a valid ${label} array`) + } + return data +} + +export async function emailBisonRecordData(response: Response, label: string): Promise { + const data = await emailBisonData(response) + if (!isRecord(data)) { + throw new Error(`Email Bison response did not include a valid ${label} object`) + } + return data +} + +export function mapLead(value: unknown): EmailBisonLead { + const record = toRecord(value) + const stats = toRecord(record.overall_stats) + + return { + id: toNullableNumber(record.id), + first_name: toStringOrNull(record.first_name), + last_name: toStringOrNull(record.last_name), + email: toStringOrNull(record.email), + title: toStringOrNull(record.title), + company: toStringOrNull(record.company), + notes: toStringOrNull(record.notes), + status: toStringOrNull(record.status), + custom_variables: toArray(record.custom_variables).map((item) => { + const variable = toRecord(item) + return { + name: toStringOrNull(variable.name), + value: toStringOrNull(variable.value), + } + }), + lead_campaign_data: toArray(record.lead_campaign_data), + overall_stats: mapLeadStats(stats), + created_at: toStringOrNull(record.created_at), + updated_at: toStringOrNull(record.updated_at), + } +} + +export function mapCampaign(value: unknown): EmailBisonCampaign { + const record = toRecord(value) + + return { + id: toNullableNumber(record.id), + uuid: toStringOrNull(record.uuid), + name: toStringOrNull(record.name), + type: toStringOrNull(record.type), + status: toStringOrNull(record.status), + emails_sent: toNullableNumber(record.emails_sent), + opened: toNullableNumber(record.opened), + unique_opens: toNullableNumber(record.unique_opens), + replied: toNullableNumber(record.replied), + unique_replies: toNullableNumber(record.unique_replies), + bounced: toNullableNumber(record.bounced), + unsubscribed: toNullableNumber(record.unsubscribed), + interested: toNullableNumber(record.interested), + total_leads_contacted: toNullableNumber(record.total_leads_contacted), + total_leads: toNullableNumber(record.total_leads), + max_emails_per_day: toNullableNumber(record.max_emails_per_day), + max_new_leads_per_day: toNullableNumber(record.max_new_leads_per_day), + plain_text: toNullableBoolean(record.plain_text), + open_tracking: toNullableBoolean(record.open_tracking), + can_unsubscribe: toNullableBoolean(record.can_unsubscribe), + unsubscribe_text: toStringOrNull(record.unsubscribe_text), + ...(record.sequence_prioritization !== undefined && { + sequence_prioritization: toStringOrNull(record.sequence_prioritization), + }), + tags: toArray(record.tags).map(mapCampaignTag), + created_at: toStringOrNull(record.created_at), + updated_at: toStringOrNull(record.updated_at), + } +} + +function mapCampaignTag(value: unknown): EmailBisonCampaignTag { + const record = toRecord(value) + + return { + id: toNullableNumber(record.id), + name: toStringOrNull(record.name), + default: toNullableBoolean(record.default), + } +} + +export function mapTag(value: unknown): EmailBisonTag { + const record = toRecord(value) + + return { + id: toNullableNumber(record.id), + name: toStringOrNull(record.name), + default: toNullableBoolean(record.default), + created_at: toStringOrNull(record.created_at), + updated_at: toStringOrNull(record.updated_at), + } +} + +export function mapReply(value: unknown): EmailBisonReply { + const record = toRecord(value) + + return { + id: toNullableNumber(record.id), + uuid: toStringOrNull(record.uuid), + folder: toStringOrNull(record.folder), + subject: toStringOrNull(record.subject), + read: toNullableBoolean(record.read), + interested: toNullableBoolean(record.interested), + automated_reply: toNullableBoolean(record.automated_reply), + html_body: toStringOrNull(record.html_body), + text_body: toStringOrNull(record.text_body), + raw_body: toStringOrNull(record.raw_body), + headers: toStringOrNull(record.headers), + date_received: toStringOrNull(record.date_received), + type: toStringOrNull(record.type), + tracked_reply: toNullableBoolean(record.tracked_reply), + scheduled_email_id: toStringNumberOrNull(record.scheduled_email_id), + campaign_id: toStringNumberOrNull(record.campaign_id), + lead_id: toNullableNumber(record.lead_id), + sender_email_id: toNullableNumber(record.sender_email_id), + raw_message_id: toStringOrNull(record.raw_message_id), + from_name: toStringOrNull(record.from_name), + from_email_address: toStringOrNull(record.from_email_address), + primary_to_email_address: toStringOrNull(record.primary_to_email_address), + to: toArray(record.to).map(mapReplyAddress), + cc: toStringOrNull(record.cc), + bcc: toStringOrNull(record.bcc), + parent_id: toStringNumberOrNull(record.parent_id), + attachments: toArray(record.attachments).map(mapReplyAttachment), + created_at: toStringOrNull(record.created_at), + updated_at: toStringOrNull(record.updated_at), + } +} + +export function actionOutput(value: unknown): { success: boolean; message: string | null } { + const record = toRecord(value) + + return { + success: record.success === true, + message: toStringOrNull(record.message), + } +} + +export const leadOutputs = { + id: { type: 'number', description: 'Lead ID' }, + first_name: { type: 'string', description: 'Lead first name' }, + last_name: { type: 'string', description: 'Lead last name' }, + email: { type: 'string', description: 'Lead email address' }, + title: { type: 'string', description: 'Lead title', optional: true }, + company: { type: 'string', description: 'Lead company', optional: true }, + notes: { type: 'string', description: 'Lead notes', optional: true }, + status: { type: 'string', description: 'Lead status', optional: true }, + custom_variables: { + type: 'array', + description: 'Lead custom variables', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Custom variable name' }, + value: { type: 'string', description: 'Custom variable value', optional: true }, + }, + }, + }, + lead_campaign_data: { + type: 'array', + description: 'Lead campaign data returned by Email Bison', + }, + overall_stats: { + type: 'object', + description: 'Lead engagement stats', + properties: { + emails_sent: { type: 'number', description: 'Emails sent' }, + opens: { type: 'number', description: 'Email opens' }, + replies: { type: 'number', description: 'Replies' }, + unique_replies: { type: 'number', description: 'Unique replies' }, + unique_opens: { type: 'number', description: 'Unique opens' }, + }, + }, + created_at: { type: 'string', description: 'Lead creation timestamp', optional: true }, + updated_at: { type: 'string', description: 'Lead update timestamp', optional: true }, +} satisfies NonNullable + +const tagProperties = { + id: { type: 'number', description: 'Tag ID' }, + name: { type: 'string', description: 'Tag name' }, + default: { type: 'boolean', description: 'Whether this is a default tag' }, + created_at: { type: 'string', description: 'Tag creation timestamp', optional: true }, + updated_at: { type: 'string', description: 'Tag update timestamp', optional: true }, +} satisfies Record + +const campaignTagProperties = { + id: { type: 'number', description: 'Tag ID' }, + name: { type: 'string', description: 'Tag name' }, + default: { type: 'boolean', description: 'Whether this is a default tag' }, +} satisfies Record + +export const listLeadsOutputs = { + leads: { + type: 'array', + description: 'List of leads', + items: { + type: 'object', + properties: leadOutputs, + }, + }, + count: { type: 'number', description: 'Number of leads returned' }, +} satisfies NonNullable + +export const campaignOutputs = { + id: { type: 'number', description: 'Campaign ID' }, + uuid: { type: 'string', description: 'Campaign UUID', optional: true }, + name: { type: 'string', description: 'Campaign name' }, + type: { type: 'string', description: 'Campaign type', optional: true }, + status: { type: 'string', description: 'Campaign status', optional: true }, + emails_sent: { type: 'number', description: 'Emails sent' }, + opened: { type: 'number', description: 'Total opens' }, + unique_opens: { type: 'number', description: 'Unique opens' }, + replied: { type: 'number', description: 'Total replies' }, + unique_replies: { type: 'number', description: 'Unique replies' }, + bounced: { type: 'number', description: 'Bounces' }, + unsubscribed: { type: 'number', description: 'Unsubscribes' }, + interested: { type: 'number', description: 'Interested replies' }, + total_leads_contacted: { type: 'number', description: 'Total leads contacted' }, + total_leads: { type: 'number', description: 'Total leads' }, + max_emails_per_day: { type: 'number', description: 'Maximum emails per day', optional: true }, + max_new_leads_per_day: { + type: 'number', + description: 'Maximum new leads per day', + optional: true, + }, + plain_text: { + type: 'boolean', + description: 'Whether campaign emails are plain text', + optional: true, + }, + open_tracking: { + type: 'boolean', + description: 'Whether open tracking is enabled', + optional: true, + }, + can_unsubscribe: { + type: 'boolean', + description: 'Whether recipients can unsubscribe', + optional: true, + }, + unsubscribe_text: { type: 'string', description: 'Unsubscribe text', optional: true }, + tags: { + type: 'array', + description: 'Campaign tags', + items: { type: 'object', properties: campaignTagProperties }, + }, + created_at: { type: 'string', description: 'Campaign creation timestamp', optional: true }, + updated_at: { type: 'string', description: 'Campaign update timestamp', optional: true }, +} satisfies NonNullable + +export const listCampaignsOutputs = { + campaigns: { + type: 'array', + description: 'List of campaigns', + items: { + type: 'object', + properties: campaignOutputs, + }, + }, + count: { type: 'number', description: 'Number of campaigns returned' }, +} satisfies NonNullable + +export const actionOutputs = { + success: { type: 'boolean', description: 'Whether the action succeeded' }, + message: { type: 'string', description: 'Action message', optional: true }, +} satisfies NonNullable + +export const listRepliesOutputs = { + replies: { + type: 'array', + description: 'List of replies', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Reply ID' }, + subject: { type: 'string', description: 'Reply subject', optional: true }, + text_body: { type: 'string', description: 'Reply text body', optional: true }, + from_email_address: { type: 'string', description: 'Sender email', optional: true }, + primary_to_email_address: { + type: 'string', + description: 'Primary recipient', + optional: true, + }, + date_received: { type: 'string', description: 'Date received', optional: true }, + interested: { type: 'boolean', description: 'Whether the reply is marked interested' }, + read: { type: 'boolean', description: 'Whether the reply is read' }, + }, + }, + }, + count: { type: 'number', description: 'Number of replies returned' }, +} satisfies NonNullable + +export const tagOutputs = { + id: { type: 'number', description: 'Tag ID' }, + name: { type: 'string', description: 'Tag name' }, + default: { type: 'boolean', description: 'Whether this is a default tag' }, + created_at: { type: 'string', description: 'Tag creation timestamp', optional: true }, + updated_at: { type: 'string', description: 'Tag update timestamp', optional: true }, +} satisfies NonNullable + +export const listTagsOutputs = { + tags: { + type: 'array', + description: 'List of tags', + items: { + type: 'object', + properties: tagProperties, + }, + }, + count: { type: 'number', description: 'Number of tags returned' }, +} satisfies NonNullable + +function mapLeadStats(record: Record): EmailBisonLeadStats { + return { + emails_sent: toNullableNumber(record.emails_sent), + opens: toNullableNumber(record.opens), + replies: toNullableNumber(record.replies), + unique_replies: toNullableNumber(record.unique_replies), + unique_opens: toNullableNumber(record.unique_opens), + } +} + +function mapReplyAddress(value: unknown): EmailBisonReplyAddress { + const record = toRecord(value) + + return { + name: toStringOrNull(record.name), + address: toStringOrNull(record.address), + } +} + +function mapReplyAttachment(value: unknown): EmailBisonReplyAttachment { + const record = toRecord(value) + + return { + id: toNullableNumber(record.id), + uuid: toStringOrNull(record.uuid), + reply_id: toNullableNumber(record.reply_id), + file_name: toStringOrNull(record.file_name), + download_url: toStringOrNull(record.download_url), + created_at: toStringOrNull(record.created_at), + updated_at: toStringOrNull(record.updated_at), + } +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} + +function toRecord(value: unknown): Record { + return isRecord(value) ? value : {} +} + +function toArray(value: unknown): unknown[] { + return Array.isArray(value) ? value : [] +} + +function toStringOrNull(value: unknown): string | null { + if (value === undefined || value === null) return null + return String(value) +} + +function toStringNumberOrNull(value: unknown): number | string | null { + if (typeof value === 'number' || typeof value === 'string') return value + return null +} + +function toNumber(value: unknown): number | null { + if (typeof value === 'number') return value + if (typeof value === 'string') { + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : null + } + return null +} + +function toNullableNumber(value: unknown): number | null { + if (value === undefined || value === null) return null + return toNumber(value) +} + +function toNullableBoolean(value: unknown): boolean | null { + if (value === undefined || value === null) return null + return typeof value === 'boolean' ? value : null +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 6bc8feea7e3..e56fb11b9e2 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -567,6 +567,21 @@ import { elasticsearchUpdateDocumentTool, } from '@/tools/elasticsearch' import { elevenLabsTtsTool } from '@/tools/elevenlabs' +import { + emailBisonAttachLeadsToCampaignTool, + emailBisonAttachTagsToLeadsTool, + emailBisonCreateCampaignTool, + emailBisonCreateLeadTool, + emailBisonCreateTagTool, + emailBisonGetLeadTool, + emailBisonListCampaignsTool, + emailBisonListLeadsTool, + emailBisonListRepliesTool, + emailBisonListTagsTool, + emailBisonUpdateCampaignStatusTool, + emailBisonUpdateCampaignTool, + emailBisonUpdateLeadTool, +} from '@/tools/emailbison' import { enrichCheckCreditsTool, enrichCompanyFundingTool, @@ -4016,6 +4031,19 @@ export const tools: Record = { elasticsearch_list_indices: elasticsearchListIndicesTool, elasticsearch_cluster_health: elasticsearchClusterHealthTool, elasticsearch_cluster_stats: elasticsearchClusterStatsTool, + emailbison_attach_leads_to_campaign: emailBisonAttachLeadsToCampaignTool, + emailbison_attach_tags_to_leads: emailBisonAttachTagsToLeadsTool, + emailbison_create_campaign: emailBisonCreateCampaignTool, + emailbison_create_lead: emailBisonCreateLeadTool, + emailbison_create_tag: emailBisonCreateTagTool, + emailbison_get_lead: emailBisonGetLeadTool, + emailbison_list_campaigns: emailBisonListCampaignsTool, + emailbison_list_leads: emailBisonListLeadsTool, + emailbison_list_replies: emailBisonListRepliesTool, + emailbison_list_tags: emailBisonListTagsTool, + emailbison_update_campaign: emailBisonUpdateCampaignTool, + emailbison_update_campaign_status: emailBisonUpdateCampaignStatusTool, + emailbison_update_lead: emailBisonUpdateLeadTool, evernote_copy_note: evernoteCopyNoteTool, evernote_create_note: evernoteCreateNoteTool, evernote_create_notebook: evernoteCreateNotebookTool, @@ -5249,13 +5277,13 @@ export const tools: Record = { hubspot_update_deal: hubspotUpdateDealTool, hubspot_update_line_item: hubspotUpdateLineItemTool, hubspot_update_ticket: hubspotUpdateTicketTool, + sharepoint_add_list_items: sharepointAddListItemTool, + sharepoint_create_list: sharepointCreateListTool, sharepoint_create_page: sharepointCreatePageTool, - sharepoint_read_page: sharepointReadPageTool, - sharepoint_list_sites: sharepointListSitesTool, sharepoint_get_list: sharepointGetListTool, - sharepoint_create_list: sharepointCreateListTool, + sharepoint_list_sites: sharepointListSitesTool, + sharepoint_read_page: sharepointReadPageTool, sharepoint_update_list: sharepointUpdateListItemTool, - sharepoint_add_list_items: sharepointAddListItemTool, sharepoint_upload_file: sharepointUploadFileTool, stripe_create_payment_intent: stripeCreatePaymentIntentTool, stripe_retrieve_payment_intent: stripeRetrievePaymentIntentTool, diff --git a/apps/sim/tools/sharepoint/add_list_items.ts b/apps/sim/tools/sharepoint/add_list_items.ts index 8f2bb197597..ab3afdf3bea 100644 --- a/apps/sim/tools/sharepoint/add_list_items.ts +++ b/apps/sim/tools/sharepoint/add_list_items.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import type { SharepointAddListItemResponse, SharepointToolParams } from '@/tools/sharepoint/types' +import { optionalTrim } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SharePointAddListItem') @@ -8,7 +9,7 @@ export const addListItemTool: ToolConfig { - const siteId = params.siteId || params.siteSelector || 'root' - if (!params.listId) { + const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root' + const listId = optionalTrim(params.listId) + if (!listId) { throw new Error('listId must be provided') } - const listSegment = params.listId + const listSegment = encodeURIComponent(listId) return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items` }, method: 'POST', @@ -74,7 +76,10 @@ export const addListItemTool: ToolConfig) && Object.keys(params.listItemFields as Record).length === 1 - ? ((params.listItemFields as any).fields as Record) + ? ((params.listItemFields as { fields: Record }).fields as Record< + string, + unknown + >) : (params.listItemFields as Record) if (!providedFields || Object.keys(providedFields).length === 0) { diff --git a/apps/sim/tools/sharepoint/create_list.ts b/apps/sim/tools/sharepoint/create_list.ts index 18d949ac7cd..007631b207e 100644 --- a/apps/sim/tools/sharepoint/create_list.ts +++ b/apps/sim/tools/sharepoint/create_list.ts @@ -5,6 +5,7 @@ import type { SharepointList, SharepointToolParams, } from '@/tools/sharepoint/types' +import { optionalTrim } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SharePointCreateList') @@ -13,7 +14,7 @@ export const createListTool: ToolConfig { - const siteId = params.siteSelector || params.siteId || 'root' + const siteId = optionalTrim(params.siteSelector) || optionalTrim(params.siteId) || 'root' return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists` }, method: 'POST', @@ -79,36 +80,51 @@ export const createListTool: ToolConfig { - if (!params.listDisplayName) { + const listDisplayName = optionalTrim(params.listDisplayName) + if (!listDisplayName) { throw new Error('listDisplayName is required') } - // Derive columns from pageContent JSON (object or string) or top-level array let columns: unknown[] | undefined if (params.pageContent) { if (typeof params.pageContent === 'string') { try { const parsed = JSON.parse(params.pageContent) - if (Array.isArray(parsed)) columns = parsed - else if (parsed && Array.isArray((parsed as any).columns)) - columns = (parsed as any).columns + if (Array.isArray(parsed)) { + columns = parsed + } else if ( + parsed && + typeof parsed === 'object' && + Array.isArray((parsed as { columns?: unknown[] }).columns) + ) { + columns = (parsed as { columns: unknown[] }).columns + } } catch (error) { logger.warn('Invalid JSON in pageContent for create list; ignoring', { error: toError(error).message, }) } } else if (typeof params.pageContent === 'object') { - const pc: any = params.pageContent - if (Array.isArray(pc)) columns = pc - else if (pc && Array.isArray(pc.columns)) columns = pc.columns + const pageContent = params.pageContent as { columns?: unknown[] } | unknown[] + if (Array.isArray(pageContent)) { + columns = pageContent + } else if (pageContent && Array.isArray(pageContent.columns)) { + columns = pageContent.columns + } } } - const payload: any = { - displayName: params.listDisplayName, - description: params.listDescription, - list: { template: params.listTemplate || 'genericList' }, + const payload: { + displayName: string + description?: string + list: { template: string } + columns?: unknown[] + } = { + displayName: listDisplayName, + list: { template: optionalTrim(params.listTemplate) || 'genericList' }, } + const listDescription = optionalTrim(params.listDescription) + if (listDescription) payload.description = listDescription if (columns && columns.length > 0) payload.columns = columns logger.info('Creating SharePoint list', { diff --git a/apps/sim/tools/sharepoint/create_page.ts b/apps/sim/tools/sharepoint/create_page.ts index c5bea57bb10..32a1e169269 100644 --- a/apps/sim/tools/sharepoint/create_page.ts +++ b/apps/sim/tools/sharepoint/create_page.ts @@ -4,6 +4,7 @@ import type { SharepointPage, SharepointToolParams, } from '@/tools/sharepoint/types' +import { optionalTrim } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SharePointCreatePage') @@ -12,7 +13,7 @@ export const createPageTool: ToolConfig { - // Use specific site if provided, otherwise use root site - const siteId = params.siteSelector || params.siteId || 'root' + const siteId = optionalTrim(params.siteSelector) || optionalTrim(params.siteId) || 'root' return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages` }, method: 'POST', @@ -71,16 +71,16 @@ export const createPageTool: ToolConfig { - if (!params.pageName) { + const pageName = optionalTrim(params.pageName) + if (!pageName) { throw new Error('Page name is required') } - const pageTitle = params.pageTitle || params.pageName + const pageTitle = optionalTrim(params.pageTitle) || pageName - // Basic page structure required by Microsoft Graph API const pageData: SharepointPage = { '@odata.type': '#microsoft.graph.sitePage', - name: params.pageName, + name: pageName, title: pageTitle, publishingState: { level: 'draft', @@ -88,8 +88,8 @@ export const createPageTool: ToolConfig${params.pageContent.replace(/"/g, '"').replace(/'/g, ''')}

`, + innerHtml: `

${pageContent.replace(/"/g, '"').replace(/'/g, ''')}

`, }, ], }, diff --git a/apps/sim/tools/sharepoint/get_list.ts b/apps/sim/tools/sharepoint/get_list.ts index f5528ee95b6..bcf3036a977 100644 --- a/apps/sim/tools/sharepoint/get_list.ts +++ b/apps/sim/tools/sharepoint/get_list.ts @@ -4,6 +4,7 @@ import type { SharepointList, SharepointToolParams, } from '@/tools/sharepoint/types' +import { assertGraphNextPageUrl, getGraphNextPageUrl, optionalTrim } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SharePointGetList') @@ -12,7 +13,7 @@ export const getListTool: ToolConfig { - const siteId = params.siteId || params.siteSelector || 'root' + if (params.nextPageUrl) { + return assertGraphNextPageUrl(params.nextPageUrl) + } + + const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root' + const listId = optionalTrim(params.listId) - // If neither listId nor listTitle provided, list all lists in the site - if (!params.listId) { + if (!listId) { const baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists` const url = new URL(baseUrl) const finalUrl = url.toString() @@ -63,11 +86,9 @@ export const getListTool: ToolConfig { + transformResponse: async (response: Response, params) => { const data = await response.json() // If the response is a collection of items (from the items endpoint) @@ -122,25 +142,18 @@ export const getListTool: ToolConfig, })) - const nextLink: string | undefined = (data as any)['@odata.nextLink'] - const nextPageToken = nextLink - ? (() => { - try { - const u = new URL(nextLink) - return u.searchParams.get('$skiptoken') || u.searchParams.get('$skip') || undefined - } catch { - return undefined - } - })() - : undefined + const nextPageUrl = getGraphNextPageUrl(data as Record) return { success: true, - output: { list: { items } as SharepointList, nextPageToken }, + output: { + list: { id: optionalTrim(params?.listId) || '', items } as SharepointList, + items, + nextPageUrl, + }, } } - // If this is a collection of lists (site-level) if (Array.isArray((data as any).value)) { const lists: SharepointList[] = (data as any).value.map((l: any) => ({ id: l.id, @@ -152,25 +165,14 @@ export const getListTool: ToolConfig { - try { - const u = new URL(nextLink) - return u.searchParams.get('$skiptoken') || u.searchParams.get('$skip') || undefined - } catch { - return undefined - } - })() - : undefined + const nextPageUrl = getGraphNextPageUrl(data as Record) return { success: true, - output: { lists, nextPageToken }, + output: { lists, nextPageUrl }, } } - // Single list response (with optional expands) const list: SharepointList = { id: data.id, displayName: data.displayName ?? data.name, @@ -242,5 +244,21 @@ export const getListTool: ToolConfig = { id: 'sharepoint_list_sites', name: 'List SharePoint Sites', description: 'List details of all SharePoint sites', - version: '1.0', + version: '1.0.0', oauth: { required: true, @@ -29,6 +30,12 @@ export const listSitesTool: ToolConfig { + if (params.nextPageUrl) { + return assertGraphNextPageUrl(params.nextPageUrl) + } + let baseUrl: string + const groupId = optionalTrim(params.groupId) + const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) - if (params.groupId) { - // Access group team site - baseUrl = `https://graph.microsoft.com/v1.0/groups/${params.groupId}/sites/root` - } else if (params.siteId || params.siteSelector) { - // Access specific site by ID - const siteId = params.siteId || params.siteSelector + if (groupId) { + baseUrl = `https://graph.microsoft.com/v1.0/groups/${groupId}/sites/root` + } else if (siteId) { baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}` } else { - // get all sites baseUrl = 'https://graph.microsoft.com/v1.0/sites?search=*' } const url = new URL(baseUrl) - // Use Microsoft Graph $select parameter to get site details url.searchParams.append( '$select', 'id,name,displayName,webUrl,description,createdDateTime,lastModifiedDateTime,isPersonalSite,root,siteCollection' @@ -71,12 +85,10 @@ export const listSitesTool: ToolConfig { + transformResponse: async (response: Response) => { const data = await response.json() - // Check if this is a search result (multiple sites) or single site if (data.value && Array.isArray(data.value)) { - // Multiple sites from search return { success: true, output: { @@ -89,10 +101,11 @@ export const listSitesTool: ToolConfig), }, } } - // Single site response + return { success: true, output: { @@ -155,5 +168,10 @@ export const listSitesTool: ToolConfig { - // Use specific site if provided, otherwise use root site - const siteId = params.siteId || params.siteSelector || 'root' + if (params.nextPageUrl) { + return assertGraphNextPageUrl(params.nextPageUrl) + } + + const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root' + const pageId = optionalTrim(params.pageId) let baseUrl: string - if (params.pageId) { - // Read specific page by ID - baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${params.pageId}` + if (pageId) { + baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageId}/microsoft.graph.sitePage` } else { - // List all pages (with optional filtering by name) - baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages` + baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/microsoft.graph.sitePage` } const url = new URL(baseUrl) - // Use Microsoft Graph $select parameter to get page details - // Only include valid properties for SharePoint pages url.searchParams.append( '$select', - 'id,name,title,webUrl,pageLayout,createdDateTime,lastModifiedDateTime' + 'id,name,title,webUrl,pageLayout,description,createdDateTime,lastModifiedDateTime' ) - // If searching by name, add filter - if (params.pageName && !params.pageId) { - // Try to handle both with and without .aspx extension - const pageName = params.pageName + if (params.pageName && !pageId) { + const pageName = params.pageName.trim() const pageNameWithAspx = pageName.endsWith('.aspx') ? pageName : `${pageName}.aspx` - - // Search for exact match first, then with .aspx if needed - url.searchParams.append('$filter', `name eq '${pageName}' or name eq '${pageNameWithAspx}'`) - url.searchParams.append('$top', '10') // Get more results to find matches - } else if (!params.pageId && !params.pageName) { - // When listing all pages, apply maxPages limit - const maxPages = Math.min(params.maxPages || 10, 50) // Default 10, max 50 + const escapedPageName = escapeODataString(pageName) + const escapedPageNameWithAspx = escapeODataString(pageNameWithAspx) + + url.searchParams.append( + '$filter', + `name eq '${escapedPageName}' or name eq '${escapedPageNameWithAspx}'` + ) + url.searchParams.append('$top', '10') + } else if (!pageId && !params.pageName) { + const requestedMaxPages = + typeof params.maxPages === 'number' ? params.maxPages : Number(params.maxPages || 10) + const maxPages = Math.min(Number.isFinite(requestedMaxPages) ? requestedMaxPages : 10, 50) url.searchParams.append('$top', maxPages.toString()) } - // Only expand content when getting a specific page by ID - if (params.pageId) { + if (pageId) { url.searchParams.append('$expand', 'canvasLayout') } @@ -112,7 +127,7 @@ export const readPageTool: ToolConfig + nextPageUrl?: string } } @@ -155,12 +158,12 @@ export interface SharepointToolParams { siteSelector?: string pageId?: string pageName?: string - pageContent?: string + pageContent?: string | unknown[] | { columns?: unknown[] } pageTitle?: string publishingState?: string query?: string pageSize?: number - pageToken?: string + nextPageUrl?: string hostname?: string serverRelativePath?: string groupId?: string @@ -188,6 +191,7 @@ export interface GraphApiResponse { id?: string name?: string title?: string + description?: string | null webUrl?: string pageLayout?: string createdDateTime?: string @@ -203,6 +207,7 @@ export interface GraphApiPageItem { id: string name: string title?: string + description?: string | null webUrl?: string pageLayout?: string createdDateTime?: string @@ -227,36 +232,6 @@ export interface CanvasLayout { }> } -export interface SharepointReadSiteResponse extends ToolResponse { - output: { - site?: { - id: string - name: string - displayName: string - webUrl: string - description?: string - createdDateTime?: string - lastModifiedDateTime?: string - isPersonalSite?: boolean - root?: { - serverRelativeUrl: string - } - siteCollection?: { - hostname: string - } - } - sites?: Array<{ - id: string - name: string - displayName: string - webUrl: string - description?: string - createdDateTime?: string - lastModifiedDateTime?: string - }> - } -} - export type SharepointResponse = | SharepointListSitesResponse | SharepointCreatePageResponse @@ -272,7 +247,8 @@ export interface SharepointGetListResponse extends ToolResponse { output: { list?: SharepointList lists?: SharepointList[] - nextPageToken?: string + items?: SharepointListItem[] + nextPageUrl?: string } } @@ -309,9 +285,25 @@ export interface SharepointUploadedFile { lastModifiedDateTime?: string } +export interface SharepointSkippedFile { + name: string + size: number + limit: number + reason: string +} + +export interface SharepointUploadError { + name: string + error: string + status?: number +} + export interface SharepointUploadFileResponse extends ToolResponse { output: { uploadedFiles: SharepointUploadedFile[] fileCount: number + skippedFiles?: SharepointSkippedFile[] + skippedCount?: number + errors?: SharepointUploadError[] } } diff --git a/apps/sim/tools/sharepoint/update_list.ts b/apps/sim/tools/sharepoint/update_list.ts index d2a62227963..a0511e45f2b 100644 --- a/apps/sim/tools/sharepoint/update_list.ts +++ b/apps/sim/tools/sharepoint/update_list.ts @@ -3,6 +3,7 @@ import type { SharepointToolParams, SharepointUpdateListItemResponse, } from '@/tools/sharepoint/types' +import { optionalTrim } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SharePointUpdateListItem') @@ -14,7 +15,7 @@ export const updateListItemTool: ToolConfig< id: 'sharepoint_update_list', name: 'Update SharePoint List Item', description: 'Update the properties (fields) on a SharePoint list item', - version: '1.0', + version: '1.0.0', oauth: { required: true, @@ -42,7 +43,7 @@ export const updateListItemTool: ToolConfig< }, listId: { type: 'string', - required: false, + required: true, visibility: 'user-or-llm', description: 'The ID of the list containing the item. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012', @@ -54,7 +55,7 @@ export const updateListItemTool: ToolConfig< description: 'The ID of the list item to update. Example: 1, 42, or 123', }, listItemFields: { - type: 'object', + type: 'json', required: true, visibility: 'user-only', description: 'Field values to update on the list item', @@ -63,13 +64,15 @@ export const updateListItemTool: ToolConfig< request: { url: (params) => { - const siteId = params.siteId || params.siteSelector || 'root' - if (!params.itemId) throw new Error('itemId is required') - if (!params.listId) { + const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root' + const itemId = optionalTrim(params.itemId) + const listId = optionalTrim(params.listId) + if (!itemId) throw new Error('itemId is required') + if (!listId) { throw new Error('listId must be provided') } - const listSegment = params.listId - return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items/${params.itemId}/fields` + const listSegment = encodeURIComponent(listId) + return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items/${encodeURIComponent(itemId)}/fields` }, method: 'PATCH', headers: (params) => ({ diff --git a/apps/sim/tools/sharepoint/upload_file.ts b/apps/sim/tools/sharepoint/upload_file.ts index 8728e4ea673..94626a82ee6 100644 --- a/apps/sim/tools/sharepoint/upload_file.ts +++ b/apps/sim/tools/sharepoint/upload_file.ts @@ -1,11 +1,12 @@ import type { SharepointToolParams, SharepointUploadFileResponse } from '@/tools/sharepoint/types' +import { optionalTrim } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' export const uploadFileTool: ToolConfig = { id: 'sharepoint_upload_file', name: 'Upload File to SharePoint', description: 'Upload files to a SharePoint document library', - version: '1.0', + version: '1.0.0', oauth: { required: true, @@ -47,7 +48,7 @@ export const uploadFileTool: ToolConfig { return { accessToken: params.accessToken, - siteId: params.siteId || 'root', - driveId: params.driveId || null, - folderPath: params.folderPath || null, - fileName: params.fileName || null, + siteId: optionalTrim(params.siteId) || 'root', + driveId: optionalTrim(params.driveId) || null, + folderPath: optionalTrim(params.folderPath) || null, + fileName: optionalTrim(params.fileName) || null, files: params.files || null, } }, @@ -73,15 +74,17 @@ export const uploadFileTool: ToolConfig { const data = await response.json() - if (!data.success) { - throw new Error(data.error || 'Failed to upload files to SharePoint') - } + const output = data.output ?? {} return { - success: true, + success: Boolean(data.success), output: { - uploadedFiles: data.output.uploadedFiles, - fileCount: data.output.fileCount, + uploadedFiles: output.uploadedFiles ?? [], + fileCount: output.fileCount ?? 0, + skippedFiles: output.skippedFiles ?? [], + skippedCount: output.skippedCount ?? 0, + errors: output.errors ?? [], }, + error: data.success ? undefined : data.error || 'Failed to upload files to SharePoint', } }, @@ -105,5 +108,38 @@ export const uploadFileTool: ToolConfig)['@odata.nextLink'] + return typeof nextLink === 'string' ? nextLink : undefined +} + +export function assertGraphNextPageUrl(nextPageUrl: string): string { + const trimmed = nextPageUrl.trim() + const url = new URL(trimmed) + if (url.origin !== 'https://graph.microsoft.com') { + throw new Error('nextPageUrl must be a Microsoft Graph @odata.nextLink URL') + } + return url.toString() +} + function stripHtmlTags(html: string): string { let text = html let previous: string diff --git a/apps/sim/triggers/emailbison/email_account_added.ts b/apps/sim/triggers/emailbison/email_account_added.ts new file mode 100644 index 00000000000..24669f31bcc --- /dev/null +++ b/apps/sim/triggers/emailbison/email_account_added.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonEmailAccountAddedOutputs, + buildEmailBisonExtraFields, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonEmailAccountAddedTrigger: TriggerConfig = { + id: 'emailbison_email_account_added', + name: 'Email Bison Email Account Added', + provider: 'emailbison', + description: 'Trigger when a sender email account is added to Email Bison', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_email_account_added', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Email Account Added'), + extraFields: buildEmailBisonExtraFields('emailbison_email_account_added'), + }), + outputs: buildEmailBisonEmailAccountAddedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/email_account_disconnected.ts b/apps/sim/triggers/emailbison/email_account_disconnected.ts new file mode 100644 index 00000000000..ea70ad995d7 --- /dev/null +++ b/apps/sim/triggers/emailbison/email_account_disconnected.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonEmailAccountDisconnectedOutputs, + buildEmailBisonExtraFields, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonEmailAccountDisconnectedTrigger: TriggerConfig = { + id: 'emailbison_email_account_disconnected', + name: 'Email Bison Email Account Disconnected', + provider: 'emailbison', + description: 'Trigger when a sender email account disconnects in Email Bison', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_email_account_disconnected', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Email Account Disconnected'), + extraFields: buildEmailBisonExtraFields('emailbison_email_account_disconnected'), + }), + outputs: buildEmailBisonEmailAccountDisconnectedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/email_account_reconnected.ts b/apps/sim/triggers/emailbison/email_account_reconnected.ts new file mode 100644 index 00000000000..3b05d36205c --- /dev/null +++ b/apps/sim/triggers/emailbison/email_account_reconnected.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonEmailAccountReconnectedOutputs, + buildEmailBisonExtraFields, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonEmailAccountReconnectedTrigger: TriggerConfig = { + id: 'emailbison_email_account_reconnected', + name: 'Email Bison Email Account Reconnected', + provider: 'emailbison', + description: 'Trigger when a sender email account reconnects in Email Bison', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_email_account_reconnected', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Email Account Reconnected'), + extraFields: buildEmailBisonExtraFields('emailbison_email_account_reconnected'), + }), + outputs: buildEmailBisonEmailAccountReconnectedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/email_account_removed.ts b/apps/sim/triggers/emailbison/email_account_removed.ts new file mode 100644 index 00000000000..6ea2038c287 --- /dev/null +++ b/apps/sim/triggers/emailbison/email_account_removed.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonEmailAccountRemovedOutputs, + buildEmailBisonExtraFields, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonEmailAccountRemovedTrigger: TriggerConfig = { + id: 'emailbison_email_account_removed', + name: 'Email Bison Email Account Removed', + provider: 'emailbison', + description: 'Trigger when a sender email account is removed from Email Bison', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_email_account_removed', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Email Account Removed'), + extraFields: buildEmailBisonExtraFields('emailbison_email_account_removed'), + }), + outputs: buildEmailBisonEmailAccountRemovedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/email_bounced.ts b/apps/sim/triggers/emailbison/email_bounced.ts new file mode 100644 index 00000000000..adfee951386 --- /dev/null +++ b/apps/sim/triggers/emailbison/email_bounced.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonEmailBouncedOutputs, + buildEmailBisonExtraFields, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonEmailBouncedTrigger: TriggerConfig = { + id: 'emailbison_email_bounced', + name: 'Email Bison Email Bounced', + provider: 'emailbison', + description: 'Trigger when an Email Bison campaign email bounces', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_email_bounced', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Email Bounced'), + extraFields: buildEmailBisonExtraFields('emailbison_email_bounced'), + }), + outputs: buildEmailBisonEmailBouncedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/email_opened.ts b/apps/sim/triggers/emailbison/email_opened.ts new file mode 100644 index 00000000000..53e7bc9e5f6 --- /dev/null +++ b/apps/sim/triggers/emailbison/email_opened.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonEmailOpenedOutputs, + buildEmailBisonExtraFields, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonEmailOpenedTrigger: TriggerConfig = { + id: 'emailbison_email_opened', + name: 'Email Bison Email Opened', + provider: 'emailbison', + description: 'Trigger when an Email Bison campaign email is opened', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_email_opened', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Email Opened'), + extraFields: buildEmailBisonExtraFields('emailbison_email_opened'), + }), + outputs: buildEmailBisonEmailOpenedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/email_sent.ts b/apps/sim/triggers/emailbison/email_sent.ts new file mode 100644 index 00000000000..3b7bcbabfd4 --- /dev/null +++ b/apps/sim/triggers/emailbison/email_sent.ts @@ -0,0 +1,27 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonEmailSentOutputs, + buildEmailBisonExtraFields, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonEmailSentTrigger: TriggerConfig = { + id: 'emailbison_email_sent', + name: 'Email Bison Email Sent', + provider: 'emailbison', + description: 'Trigger when a campaign email is sent in Email Bison', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_email_sent', + triggerOptions: emailBisonTriggerOptions, + includeDropdown: true, + setupInstructions: emailBisonSetupInstructions('Email Sent'), + extraFields: buildEmailBisonExtraFields('emailbison_email_sent'), + }), + outputs: buildEmailBisonEmailSentOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/index.ts b/apps/sim/triggers/emailbison/index.ts new file mode 100644 index 00000000000..7876e0731c6 --- /dev/null +++ b/apps/sim/triggers/emailbison/index.ts @@ -0,0 +1,17 @@ +export { emailBisonEmailAccountAddedTrigger } from '@/triggers/emailbison/email_account_added' +export { emailBisonEmailAccountDisconnectedTrigger } from '@/triggers/emailbison/email_account_disconnected' +export { emailBisonEmailAccountReconnectedTrigger } from '@/triggers/emailbison/email_account_reconnected' +export { emailBisonEmailAccountRemovedTrigger } from '@/triggers/emailbison/email_account_removed' +export { emailBisonEmailBouncedTrigger } from '@/triggers/emailbison/email_bounced' +export { emailBisonEmailOpenedTrigger } from '@/triggers/emailbison/email_opened' +export { emailBisonEmailSentTrigger } from '@/triggers/emailbison/email_sent' +export { emailBisonLeadFirstContactedTrigger } from '@/triggers/emailbison/lead_first_contacted' +export { emailBisonLeadInterestedTrigger } from '@/triggers/emailbison/lead_interested' +export { emailBisonLeadRepliedTrigger } from '@/triggers/emailbison/lead_replied' +export { emailBisonLeadUnsubscribedTrigger } from '@/triggers/emailbison/lead_unsubscribed' +export { emailBisonManualEmailSentTrigger } from '@/triggers/emailbison/manual_email_sent' +export { emailBisonTagAttachedTrigger } from '@/triggers/emailbison/tag_attached' +export { emailBisonTagRemovedTrigger } from '@/triggers/emailbison/tag_removed' +export { emailBisonUntrackedReplyReceivedTrigger } from '@/triggers/emailbison/untracked_reply_received' +export { emailBisonWarmupDisabledCausingBouncesTrigger } from '@/triggers/emailbison/warmup_disabled_causing_bounces' +export { emailBisonWarmupDisabledReceivingBouncesTrigger } from '@/triggers/emailbison/warmup_disabled_receiving_bounces' diff --git a/apps/sim/triggers/emailbison/lead_first_contacted.ts b/apps/sim/triggers/emailbison/lead_first_contacted.ts new file mode 100644 index 00000000000..406940ce0b0 --- /dev/null +++ b/apps/sim/triggers/emailbison/lead_first_contacted.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonExtraFields, + buildEmailBisonLeadFirstContactedOutputs, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonLeadFirstContactedTrigger: TriggerConfig = { + id: 'emailbison_lead_first_contacted', + name: 'Email Bison Contact First Emailed', + provider: 'emailbison', + description: 'Trigger when a contact receives their first campaign email in Email Bison', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_lead_first_contacted', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Contact First Emailed'), + extraFields: buildEmailBisonExtraFields('emailbison_lead_first_contacted'), + }), + outputs: buildEmailBisonLeadFirstContactedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/lead_interested.ts b/apps/sim/triggers/emailbison/lead_interested.ts new file mode 100644 index 00000000000..b4fbdbd5202 --- /dev/null +++ b/apps/sim/triggers/emailbison/lead_interested.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonExtraFields, + buildEmailBisonLeadInterestedOutputs, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonLeadInterestedTrigger: TriggerConfig = { + id: 'emailbison_lead_interested', + name: 'Email Bison Contact Interested', + provider: 'emailbison', + description: 'Trigger when a reply is marked interested in Email Bison', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_lead_interested', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Contact Interested'), + extraFields: buildEmailBisonExtraFields('emailbison_lead_interested'), + }), + outputs: buildEmailBisonLeadInterestedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/lead_replied.ts b/apps/sim/triggers/emailbison/lead_replied.ts new file mode 100644 index 00000000000..6b47967e42a --- /dev/null +++ b/apps/sim/triggers/emailbison/lead_replied.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonExtraFields, + buildEmailBisonLeadRepliedOutputs, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonLeadRepliedTrigger: TriggerConfig = { + id: 'emailbison_lead_replied', + name: 'Email Bison Contact Replied', + provider: 'emailbison', + description: 'Trigger when a campaign lead replies in Email Bison', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_lead_replied', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Contact Replied'), + extraFields: buildEmailBisonExtraFields('emailbison_lead_replied'), + }), + outputs: buildEmailBisonLeadRepliedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/lead_unsubscribed.ts b/apps/sim/triggers/emailbison/lead_unsubscribed.ts new file mode 100644 index 00000000000..ce7f5f7aef4 --- /dev/null +++ b/apps/sim/triggers/emailbison/lead_unsubscribed.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonExtraFields, + buildEmailBisonLeadUnsubscribedOutputs, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonLeadUnsubscribedTrigger: TriggerConfig = { + id: 'emailbison_lead_unsubscribed', + name: 'Email Bison Contact Unsubscribed', + provider: 'emailbison', + description: 'Trigger when a contact unsubscribes in Email Bison', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_lead_unsubscribed', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Contact Unsubscribed'), + extraFields: buildEmailBisonExtraFields('emailbison_lead_unsubscribed'), + }), + outputs: buildEmailBisonLeadUnsubscribedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/manual_email_sent.ts b/apps/sim/triggers/emailbison/manual_email_sent.ts new file mode 100644 index 00000000000..772b88e5c06 --- /dev/null +++ b/apps/sim/triggers/emailbison/manual_email_sent.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonExtraFields, + buildEmailBisonManualEmailSentOutputs, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonManualEmailSentTrigger: TriggerConfig = { + id: 'emailbison_manual_email_sent', + name: 'Email Bison Manual Email Sent', + provider: 'emailbison', + description: 'Trigger when a manual email is sent in Email Bison', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_manual_email_sent', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Manual Email Sent'), + extraFields: buildEmailBisonExtraFields('emailbison_manual_email_sent'), + }), + outputs: buildEmailBisonManualEmailSentOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/tag_attached.ts b/apps/sim/triggers/emailbison/tag_attached.ts new file mode 100644 index 00000000000..6d02e97f107 --- /dev/null +++ b/apps/sim/triggers/emailbison/tag_attached.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonExtraFields, + buildEmailBisonTagAttachedOutputs, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonTagAttachedTrigger: TriggerConfig = { + id: 'emailbison_tag_attached', + name: 'Email Bison Tag Attached', + provider: 'emailbison', + description: 'Trigger when a custom tag is attached to a taggable in Email Bison', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_tag_attached', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Tag Attached'), + extraFields: buildEmailBisonExtraFields('emailbison_tag_attached'), + }), + outputs: buildEmailBisonTagAttachedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/tag_removed.ts b/apps/sim/triggers/emailbison/tag_removed.ts new file mode 100644 index 00000000000..cd128f8ec09 --- /dev/null +++ b/apps/sim/triggers/emailbison/tag_removed.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonExtraFields, + buildEmailBisonTagRemovedOutputs, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonTagRemovedTrigger: TriggerConfig = { + id: 'emailbison_tag_removed', + name: 'Email Bison Tag Removed', + provider: 'emailbison', + description: 'Trigger when a custom tag is removed from a taggable in Email Bison', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_tag_removed', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Tag Removed'), + extraFields: buildEmailBisonExtraFields('emailbison_tag_removed'), + }), + outputs: buildEmailBisonTagRemovedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/untracked_reply_received.ts b/apps/sim/triggers/emailbison/untracked_reply_received.ts new file mode 100644 index 00000000000..6dbe61bd0ba --- /dev/null +++ b/apps/sim/triggers/emailbison/untracked_reply_received.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonExtraFields, + buildEmailBisonUntrackedReplyReceivedOutputs, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonUntrackedReplyReceivedTrigger: TriggerConfig = { + id: 'emailbison_untracked_reply_received', + name: 'Email Bison Untracked Reply Received', + provider: 'emailbison', + description: 'Trigger when Email Bison receives a reply not tied to a scheduled campaign email', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_untracked_reply_received', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Untracked Reply Received'), + extraFields: buildEmailBisonExtraFields('emailbison_untracked_reply_received'), + }), + outputs: buildEmailBisonUntrackedReplyReceivedOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/utils.ts b/apps/sim/triggers/emailbison/utils.ts new file mode 100644 index 00000000000..12d9ebbe789 --- /dev/null +++ b/apps/sim/triggers/emailbison/utils.ts @@ -0,0 +1,510 @@ +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +export const EMAILBISON_TRIGGER_TO_EVENT_TYPE = { + emailbison_email_sent: 'email_sent', + emailbison_lead_first_contacted: 'lead_first_contacted', + emailbison_lead_replied: 'lead_replied', + emailbison_lead_interested: 'lead_interested', + emailbison_lead_unsubscribed: 'lead_unsubscribed', + emailbison_untracked_reply_received: 'untracked_reply_received', + emailbison_email_opened: 'email_opened', + emailbison_email_bounced: 'email_bounced', + emailbison_email_account_added: 'email_account_added', + emailbison_email_account_removed: 'email_account_removed', + emailbison_email_account_disconnected: 'email_account_disconnected', + emailbison_email_account_reconnected: 'email_account_reconnected', + emailbison_manual_email_sent: 'manual_email_sent', + emailbison_tag_attached: 'tag_attached', + emailbison_tag_removed: 'tag_removed', + emailbison_warmup_disabled_receiving_bounces: 'warmup_disabled_receiving_bounces', + emailbison_warmup_disabled_causing_bounces: 'warmup_disabled_causing_bounces', +} as const + +export const emailBisonTriggerOptions = [ + { label: 'Email Sent', id: 'emailbison_email_sent' }, + { label: 'Contact First Emailed', id: 'emailbison_lead_first_contacted' }, + { label: 'Contact Replied', id: 'emailbison_lead_replied' }, + { label: 'Contact Interested', id: 'emailbison_lead_interested' }, + { label: 'Contact Unsubscribed', id: 'emailbison_lead_unsubscribed' }, + { label: 'Untracked Reply Received', id: 'emailbison_untracked_reply_received' }, + { label: 'Email Opened', id: 'emailbison_email_opened' }, + { label: 'Email Bounced', id: 'emailbison_email_bounced' }, + { label: 'Email Account Added', id: 'emailbison_email_account_added' }, + { label: 'Email Account Removed', id: 'emailbison_email_account_removed' }, + { label: 'Email Account Disconnected', id: 'emailbison_email_account_disconnected' }, + { label: 'Email Account Reconnected', id: 'emailbison_email_account_reconnected' }, + { label: 'Manual Email Sent', id: 'emailbison_manual_email_sent' }, + { label: 'Tag Attached', id: 'emailbison_tag_attached' }, + { label: 'Tag Removed', id: 'emailbison_tag_removed' }, + { + label: 'Warmup Disabled Receiving Bounces', + id: 'emailbison_warmup_disabled_receiving_bounces', + }, + { + label: 'Warmup Disabled Causing Bounces', + id: 'emailbison_warmup_disabled_causing_bounces', + }, +] + +export function emailBisonSetupInstructions(eventType: string): string { + const instructions = [ + 'Create an Email Bison API token in Settings > Developer API.', + 'Enter the Instance URL from Email Bison’s webhook payload, Full API Reference, or exported Postman collection.', + `Click Save Configuration to automatically create an Email Bison webhook for ${eventType}.`, + 'The webhook will be automatically deleted from Email Bison when this trigger is removed.', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +export function buildEmailBisonExtraFields(triggerId: string): SubBlockConfig[] { + return [ + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Email Bison API token', + password: true, + required: true, + paramVisibility: 'user-only', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: 'apiBaseUrl', + title: 'Instance URL', + type: 'short-input', + placeholder: 'https://your-emailbison-workspace.com', + required: true, + paramVisibility: 'user-only', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +export function buildEmailBisonOutputs(): Record { + return { + eventType: { type: 'string', description: 'Email Bison webhook event type' }, + eventName: { type: 'string', description: 'Human-readable Email Bison event name' }, + instanceUrl: { type: 'string', description: 'Email Bison instance URL' }, + workspaceId: { type: 'number', description: 'Email Bison workspace ID' }, + workspaceName: { type: 'string', description: 'Email Bison workspace name' }, + event: { type: 'json', description: 'Raw Email Bison event metadata object' }, + data: { type: 'json', description: 'Raw Email Bison event data object' }, + } +} + +export function buildEmailBisonEmailSentOutputs(): Record { + return { + ...buildEmailBisonOutputs(), + scheduledEmail: { + id: { type: 'number', description: 'Scheduled email ID' }, + lead_id: { type: 'number', description: 'Lead ID' }, + sequence_step_id: { type: 'number', description: 'Sequence step ID' }, + sequence_step_order: { type: 'number', description: 'Sequence step order' }, + sequence_step_variant: { type: 'number', description: 'Sequence step variant' }, + email_subject: { type: 'string', description: 'Email subject' }, + email_body: { type: 'string', description: 'Email body HTML' }, + status: { type: 'string', description: 'Scheduled email status' }, + scheduled_date_est: { type: 'string', description: 'Scheduled date in EST' }, + scheduled_date_local: { type: 'string', description: 'Scheduled date in local timezone' }, + local_timezone: { type: 'string', description: 'Scheduled email local timezone' }, + sent_at: { type: 'string', description: 'Email sent timestamp' }, + opens: { type: 'number', description: 'Open count' }, + replies: { type: 'number', description: 'Reply count' }, + unique_opens: { type: 'number', description: 'Unique open count' }, + unique_replies: { type: 'number', description: 'Unique reply count' }, + interested: { type: 'string', description: 'Interested status' }, + raw_message_id: { type: 'string', description: 'Raw email message ID' }, + }, + campaignEvent: { + id: { type: 'number', description: 'Campaign event ID' }, + event_type: { type: 'string', description: 'Campaign event type' }, + created_at_local: { type: 'string', description: 'Campaign event local creation timestamp' }, + local_timezone: { type: 'string', description: 'Campaign event local timezone' }, + created_at: { type: 'string', description: 'Campaign event creation timestamp' }, + }, + lead: { + id: { type: 'number', description: 'Lead ID' }, + email: { type: 'string', description: 'Lead email address' }, + first_name: { type: 'string', description: 'Lead first name' }, + last_name: { type: 'string', description: 'Lead last name' }, + status: { type: 'string', description: 'Lead status' }, + title: { type: 'string', description: 'Lead title' }, + company: { type: 'string', description: 'Lead company' }, + custom_variables: { type: 'json', description: 'Lead custom variables' }, + emails_sent: { type: 'number', description: 'Lead emails sent count' }, + opens: { type: 'number', description: 'Lead open count' }, + unique_opens: { type: 'number', description: 'Lead unique open count' }, + replies: { type: 'number', description: 'Lead reply count' }, + unique_replies: { type: 'number', description: 'Lead unique reply count' }, + bounces: { type: 'number', description: 'Lead bounce count' }, + }, + campaign: { + id: { type: 'number', description: 'Campaign ID' }, + name: { type: 'string', description: 'Campaign name' }, + }, + senderEmail: { + id: { type: 'number', description: 'Sender email ID' }, + name: { type: 'string', description: 'Sender email name' }, + email: { type: 'string', description: 'Sender email address' }, + status: { type: 'string', description: 'Sender email status' }, + account_type: { type: 'string', description: 'Sender email connection type' }, + daily_limit: { type: 'number', description: 'Sender email daily limit' }, + emails_sent: { type: 'number', description: 'Sender email sent count' }, + replied: { type: 'number', description: 'Sender email replied count' }, + opened: { type: 'number', description: 'Sender email opened count' }, + unsubscribed: { type: 'number', description: 'Sender email unsubscribed count' }, + bounced: { type: 'number', description: 'Sender email bounced count' }, + unique_replies: { type: 'number', description: 'Sender email unique reply count' }, + unique_opens: { type: 'number', description: 'Sender email unique open count' }, + total_leads_contacted: { type: 'number', description: 'Sender email total leads contacted' }, + interested: { type: 'number', description: 'Sender email interested count' }, + created_at: { type: 'string', description: 'Sender email creation timestamp' }, + updated_at: { type: 'string', description: 'Sender email update timestamp' }, + }, + } +} + +export function buildEmailBisonLeadFirstContactedOutputs(): Record { + return buildEmailBisonEmailSentOutputs() +} + +export function buildEmailBisonLeadUnsubscribedOutputs(): Record { + return buildEmailBisonEmailSentOutputs() +} + +export function buildEmailBisonEmailOpenedOutputs(): Record { + return buildEmailBisonEmailSentOutputs() +} + +export function buildEmailBisonLeadRepliedOutputs(): Record { + return { + ...buildEmailBisonOutputs(), + reply: { + id: { type: 'number', description: 'Reply ID' }, + uuid: { type: 'string', description: 'Reply UUID' }, + email_subject: { type: 'string', description: 'Reply email subject' }, + interested: { type: 'boolean', description: 'Whether the reply is marked interested' }, + automated_reply: { type: 'boolean', description: 'Whether the reply is automated' }, + html_body: { type: 'string', description: 'Reply HTML body' }, + text_body: { type: 'string', description: 'Reply plain text body' }, + raw_body: { type: 'string', description: 'Raw MIME reply body' }, + headers: { type: 'string', description: 'Encoded raw email headers' }, + date_received: { type: 'string', description: 'Reply received timestamp' }, + from_name: { type: 'string', description: 'Reply sender name' }, + from_email_address: { type: 'string', description: 'Reply sender email address' }, + primary_to_email_address: { type: 'string', description: 'Primary recipient email address' }, + to: { type: 'json', description: 'Reply To recipients' }, + cc: { type: 'json', description: 'Reply CC recipients' }, + bcc: { type: 'json', description: 'Reply BCC recipients' }, + parent_id: { type: 'number', description: 'Parent reply ID' }, + reply_type: { type: 'string', description: 'Reply type' }, + folder: { type: 'string', description: 'Reply folder' }, + raw_message_id: { type: 'string', description: 'Raw email message ID' }, + created_at: { type: 'string', description: 'Reply creation timestamp' }, + updated_at: { type: 'string', description: 'Reply update timestamp' }, + attachments: { type: 'json', description: 'Reply attachments' }, + }, + campaignEvent: { + id: { type: 'number', description: 'Campaign event ID' }, + event_type: { type: 'string', description: 'Campaign event type' }, + created_at_local: { type: 'string', description: 'Campaign event local creation timestamp' }, + local_timezone: { type: 'string', description: 'Campaign event local timezone' }, + created_at: { type: 'string', description: 'Campaign event creation timestamp' }, + }, + lead: { + id: { type: 'number', description: 'Lead ID' }, + email: { type: 'string', description: 'Lead email address' }, + first_name: { type: 'string', description: 'Lead first name' }, + last_name: { type: 'string', description: 'Lead last name' }, + status: { type: 'string', description: 'Lead status' }, + title: { type: 'string', description: 'Lead title' }, + company: { type: 'string', description: 'Lead company' }, + custom_variables: { type: 'json', description: 'Lead custom variables' }, + emails_sent: { type: 'number', description: 'Lead emails sent count' }, + opens: { type: 'number', description: 'Lead open count' }, + unique_opens: { type: 'number', description: 'Lead unique open count' }, + replies: { type: 'number', description: 'Lead reply count' }, + unique_replies: { type: 'number', description: 'Lead unique reply count' }, + bounces: { type: 'number', description: 'Lead bounce count' }, + }, + campaign: { + id: { type: 'number', description: 'Campaign ID' }, + name: { type: 'string', description: 'Campaign name' }, + }, + scheduledEmail: { + id: { type: 'number', description: 'Scheduled email ID' }, + sequence_step_id: { type: 'number', description: 'Sequence step ID' }, + sequence_step_order: { type: 'number', description: 'Sequence step order' }, + sequence_step_variant: { type: 'number', description: 'Sequence step variant' }, + status: { type: 'string', description: 'Scheduled email status' }, + scheduled_date_est: { type: 'string', description: 'Scheduled date in EST' }, + scheduled_date_local: { type: 'string', description: 'Scheduled date in local timezone' }, + local_timezone: { type: 'string', description: 'Scheduled email local timezone' }, + sent_at: { type: 'string', description: 'Email sent timestamp' }, + opens: { type: 'number', description: 'Open count' }, + replies: { type: 'number', description: 'Reply count' }, + unique_opens: { type: 'number', description: 'Unique open count' }, + unique_replies: { type: 'number', description: 'Unique reply count' }, + interested: { type: 'string', description: 'Interested status' }, + raw_message_id: { type: 'string', description: 'Raw email message ID' }, + }, + senderEmail: { + id: { type: 'number', description: 'Sender email ID' }, + name: { type: 'string', description: 'Sender email name' }, + email: { type: 'string', description: 'Sender email address' }, + status: { type: 'string', description: 'Sender email status' }, + account_type: { type: 'string', description: 'Sender email connection type' }, + daily_limit: { type: 'number', description: 'Sender email daily limit' }, + emails_sent: { type: 'number', description: 'Sender email sent count' }, + replied: { type: 'number', description: 'Sender email replied count' }, + opened: { type: 'number', description: 'Sender email opened count' }, + unsubscribed: { type: 'number', description: 'Sender email unsubscribed count' }, + bounced: { type: 'number', description: 'Sender email bounced count' }, + unique_replies: { type: 'number', description: 'Sender email unique reply count' }, + unique_opens: { type: 'number', description: 'Sender email unique open count' }, + total_leads_contacted: { type: 'number', description: 'Sender email total leads contacted' }, + interested: { type: 'number', description: 'Sender email interested count' }, + created_at: { type: 'string', description: 'Sender email creation timestamp' }, + updated_at: { type: 'string', description: 'Sender email update timestamp' }, + }, + } +} + +export function buildEmailBisonLeadInterestedOutputs(): Record { + return buildEmailBisonLeadRepliedOutputs() +} + +export function buildEmailBisonEmailBouncedOutputs(): Record { + return buildEmailBisonLeadRepliedOutputs() +} + +export function buildEmailBisonManualEmailSentOutputs(): Record { + return { + ...buildEmailBisonOutputs(), + reply: { + id: { type: 'number', description: 'Reply ID' }, + email_subject: { type: 'string', description: 'Reply email subject' }, + interested: { type: 'boolean', description: 'Whether the reply is marked interested' }, + automated_reply: { type: 'boolean', description: 'Whether the reply is automated' }, + html_body: { type: 'string', description: 'Reply HTML body' }, + text_body: { type: 'string', description: 'Reply plain text body' }, + raw_body: { type: 'string', description: 'Raw MIME reply body' }, + headers: { type: 'string', description: 'Encoded raw email headers' }, + date_received: { type: 'string', description: 'Reply received timestamp' }, + reply_type: { type: 'string', description: 'Reply type' }, + from_name: { type: 'string', description: 'Reply sender name' }, + from_email_address: { type: 'string', description: 'Reply sender email address' }, + primary_to_email_address: { type: 'string', description: 'Primary recipient email address' }, + to: { type: 'json', description: 'Reply To recipients' }, + cc: { type: 'json', description: 'Reply CC recipients' }, + bcc: { type: 'json', description: 'Reply BCC recipients' }, + parent_id: { type: 'json', description: 'Parent reply ID' }, + folder: { type: 'string', description: 'Reply folder' }, + raw_message_id: { type: 'string', description: 'Raw email message ID' }, + created_at: { type: 'string', description: 'Reply creation timestamp' }, + updated_at: { type: 'string', description: 'Reply update timestamp' }, + attachments: { type: 'json', description: 'Reply attachments' }, + }, + lead: { + id: { type: 'number', description: 'Lead ID' }, + email: { type: 'string', description: 'Lead email address' }, + first_name: { type: 'string', description: 'Lead first name' }, + last_name: { type: 'string', description: 'Lead last name' }, + status: { type: 'string', description: 'Lead status' }, + title: { type: 'string', description: 'Lead title' }, + company: { type: 'string', description: 'Lead company' }, + custom_variables: { type: 'json', description: 'Lead custom variables' }, + emails_sent: { type: 'number', description: 'Lead emails sent count' }, + opens: { type: 'number', description: 'Lead open count' }, + unique_opens: { type: 'number', description: 'Lead unique open count' }, + replies: { type: 'number', description: 'Lead reply count' }, + unique_replies: { type: 'number', description: 'Lead unique reply count' }, + bounces: { type: 'number', description: 'Lead bounce count' }, + }, + campaign: { + id: { type: 'number', description: 'Campaign ID' }, + name: { type: 'string', description: 'Campaign name' }, + }, + scheduledEmail: { + id: { type: 'number', description: 'Scheduled email ID' }, + sequence_step_id: { type: 'number', description: 'Sequence step ID' }, + sequence_step_order: { type: 'number', description: 'Sequence step order' }, + sequence_step_variant: { type: 'number', description: 'Sequence step variant' }, + status: { type: 'string', description: 'Scheduled email status' }, + scheduled_date_est: { type: 'string', description: 'Scheduled date in EST' }, + scheduled_date_local: { type: 'string', description: 'Scheduled date in local timezone' }, + local_timezone: { type: 'string', description: 'Scheduled email local timezone' }, + sent_at: { type: 'string', description: 'Email sent timestamp' }, + opens: { type: 'number', description: 'Open count' }, + replies: { type: 'number', description: 'Reply count' }, + unique_opens: { type: 'number', description: 'Unique open count' }, + unique_replies: { type: 'number', description: 'Unique reply count' }, + interested: { type: 'json', description: 'Interested status' }, + raw_message_id: { type: 'string', description: 'Raw email message ID' }, + }, + senderEmail: { + id: { type: 'number', description: 'Sender email ID' }, + name: { type: 'string', description: 'Sender email name' }, + email: { type: 'string', description: 'Sender email address' }, + status: { type: 'string', description: 'Sender email status' }, + account_type: { type: 'string', description: 'Sender email connection type' }, + daily_limit: { type: 'number', description: 'Sender email daily limit' }, + emails_sent: { type: 'number', description: 'Sender email sent count' }, + replied: { type: 'number', description: 'Sender email replied count' }, + opened: { type: 'number', description: 'Sender email opened count' }, + unsubscribed: { type: 'number', description: 'Sender email unsubscribed count' }, + bounced: { type: 'number', description: 'Sender email bounced count' }, + unique_replies: { type: 'number', description: 'Sender email unique reply count' }, + unique_opens: { type: 'number', description: 'Sender email unique open count' }, + total_leads_contacted: { type: 'number', description: 'Sender email total leads contacted' }, + interested: { type: 'number', description: 'Sender email interested count' }, + created_at: { type: 'string', description: 'Sender email creation timestamp' }, + updated_at: { type: 'string', description: 'Sender email update timestamp' }, + }, + } +} + +export function buildEmailBisonTagAttachedOutputs(): Record { + return { + ...buildEmailBisonOutputs(), + tagId: { type: 'number', description: 'Email Bison tag ID' }, + tagName: { type: 'string', description: 'Email Bison tag name' }, + taggableId: { type: 'number', description: 'ID of the tagged resource' }, + taggableType: { type: 'string', description: 'Type of the tagged resource' }, + } +} + +export function buildEmailBisonTagRemovedOutputs(): Record { + return buildEmailBisonTagAttachedOutputs() +} + +export function buildEmailBisonEmailAccountAddedOutputs(): Record { + return { + ...buildEmailBisonOutputs(), + senderEmail: { + id: { type: 'number', description: 'Sender email ID' }, + name: { type: 'string', description: 'Sender email name' }, + email: { type: 'string', description: 'Sender email address' }, + status: { type: 'string', description: 'Sender email status' }, + account_type: { type: 'string', description: 'Sender email connection type' }, + daily_limit: { type: 'number', description: 'Sender email daily limit' }, + emails_sent: { type: 'number', description: 'Sender email sent count' }, + replied: { type: 'number', description: 'Sender email replied count' }, + opened: { type: 'number', description: 'Sender email opened count' }, + unsubscribed: { type: 'number', description: 'Sender email unsubscribed count' }, + bounced: { type: 'number', description: 'Sender email bounced count' }, + unique_replies: { type: 'number', description: 'Sender email unique reply count' }, + unique_opens: { type: 'number', description: 'Sender email unique open count' }, + total_leads_contacted: { type: 'number', description: 'Sender email total leads contacted' }, + interested: { type: 'number', description: 'Sender email interested count' }, + created_at: { type: 'string', description: 'Sender email creation timestamp' }, + updated_at: { type: 'string', description: 'Sender email update timestamp' }, + }, + } +} + +export function buildEmailBisonEmailAccountRemovedOutputs(): Record { + return buildEmailBisonEmailAccountAddedOutputs() +} + +export function buildEmailBisonEmailAccountDisconnectedOutputs(): Record { + return buildEmailBisonEmailAccountAddedOutputs() +} + +export function buildEmailBisonEmailAccountReconnectedOutputs(): Record { + return buildEmailBisonEmailAccountAddedOutputs() +} + +export function buildEmailBisonWarmupDisabledReceivingBouncesOutputs(): Record< + string, + TriggerOutput +> { + return buildEmailBisonEmailAccountAddedOutputs() +} + +export function buildEmailBisonWarmupDisabledCausingBouncesOutputs(): Record< + string, + TriggerOutput +> { + return buildEmailBisonEmailAccountAddedOutputs() +} + +export function buildEmailBisonUntrackedReplyReceivedOutputs(): Record { + return { + ...buildEmailBisonOutputs(), + reply: { + id: { type: 'number', description: 'Reply ID' }, + uuid: { type: 'string', description: 'Reply UUID' }, + email_subject: { type: 'string', description: 'Reply email subject' }, + interested: { type: 'boolean', description: 'Whether the reply is marked interested' }, + automated_reply: { type: 'boolean', description: 'Whether the reply is automated' }, + html_body: { type: 'string', description: 'Reply HTML body' }, + text_body: { type: 'string', description: 'Reply plain text body' }, + raw_body: { type: 'string', description: 'Raw MIME reply body' }, + headers: { type: 'string', description: 'Encoded raw email headers' }, + date_received: { type: 'string', description: 'Reply received timestamp' }, + from_name: { type: 'string', description: 'Reply sender name' }, + from_email_address: { type: 'string', description: 'Reply sender email address' }, + primary_to_email_address: { type: 'string', description: 'Primary recipient email address' }, + to: { type: 'json', description: 'Reply To recipients' }, + cc: { type: 'json', description: 'Reply CC recipients' }, + bcc: { type: 'json', description: 'Reply BCC recipients' }, + parent_id: { type: 'number', description: 'Parent reply ID' }, + reply_type: { type: 'string', description: 'Reply type' }, + folder: { type: 'string', description: 'Reply folder' }, + raw_message_id: { type: 'string', description: 'Raw email message ID' }, + created_at: { type: 'string', description: 'Reply creation timestamp' }, + updated_at: { type: 'string', description: 'Reply update timestamp' }, + attachments: { type: 'json', description: 'Reply attachments' }, + }, + senderEmail: { + id: { type: 'number', description: 'Sender email ID' }, + name: { type: 'string', description: 'Sender email name' }, + email: { type: 'string', description: 'Sender email address' }, + status: { type: 'string', description: 'Sender email status' }, + account_type: { type: 'string', description: 'Sender email connection type' }, + daily_limit: { type: 'number', description: 'Sender email daily limit' }, + emails_sent: { type: 'number', description: 'Sender email sent count' }, + replied: { type: 'number', description: 'Sender email replied count' }, + opened: { type: 'number', description: 'Sender email opened count' }, + unsubscribed: { type: 'number', description: 'Sender email unsubscribed count' }, + bounced: { type: 'number', description: 'Sender email bounced count' }, + unique_replies: { type: 'number', description: 'Sender email unique reply count' }, + unique_opens: { type: 'number', description: 'Sender email unique open count' }, + total_leads_contacted: { type: 'number', description: 'Sender email total leads contacted' }, + interested: { type: 'number', description: 'Sender email interested count' }, + created_at: { type: 'string', description: 'Sender email creation timestamp' }, + updated_at: { type: 'string', description: 'Sender email update timestamp' }, + }, + } +} + +export function getEmailBisonEventTypeForTrigger(triggerId: string): string | undefined { + return EMAILBISON_TRIGGER_TO_EVENT_TYPE[ + triggerId as keyof typeof EMAILBISON_TRIGGER_TO_EVENT_TYPE + ] +} + +export function isEmailBisonEventMatch(triggerId: string, body: Record): boolean { + const expectedEventType = getEmailBisonEventTypeForTrigger(triggerId) + if (!expectedEventType) return false + + const event = body.event + if (!isRecord(event)) return false + + const actualEventType = event.type + return typeof actualEventType === 'string' && actualEventType.toLowerCase() === expectedEventType +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} diff --git a/apps/sim/triggers/emailbison/warmup_disabled_causing_bounces.ts b/apps/sim/triggers/emailbison/warmup_disabled_causing_bounces.ts new file mode 100644 index 00000000000..341138bb833 --- /dev/null +++ b/apps/sim/triggers/emailbison/warmup_disabled_causing_bounces.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonExtraFields, + buildEmailBisonWarmupDisabledCausingBouncesOutputs, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonWarmupDisabledCausingBouncesTrigger: TriggerConfig = { + id: 'emailbison_warmup_disabled_causing_bounces', + name: 'Email Bison Warmup Disabled Causing Bounces', + provider: 'emailbison', + description: 'Trigger when warmup is disabled for a sender email causing too many bounces', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_warmup_disabled_causing_bounces', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Warmup Disabled Causing Bounces'), + extraFields: buildEmailBisonExtraFields('emailbison_warmup_disabled_causing_bounces'), + }), + outputs: buildEmailBisonWarmupDisabledCausingBouncesOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/emailbison/warmup_disabled_receiving_bounces.ts b/apps/sim/triggers/emailbison/warmup_disabled_receiving_bounces.ts new file mode 100644 index 00000000000..6ecfcf2b4e1 --- /dev/null +++ b/apps/sim/triggers/emailbison/warmup_disabled_receiving_bounces.ts @@ -0,0 +1,26 @@ +import { EmailBisonIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildEmailBisonExtraFields, + buildEmailBisonWarmupDisabledReceivingBouncesOutputs, + emailBisonSetupInstructions, + emailBisonTriggerOptions, +} from '@/triggers/emailbison/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const emailBisonWarmupDisabledReceivingBouncesTrigger: TriggerConfig = { + id: 'emailbison_warmup_disabled_receiving_bounces', + name: 'Email Bison Warmup Disabled Receiving Bounces', + provider: 'emailbison', + description: 'Trigger when warmup is disabled for a sender email receiving too many bounces', + version: '1.0.0', + icon: EmailBisonIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'emailbison_warmup_disabled_receiving_bounces', + triggerOptions: emailBisonTriggerOptions, + setupInstructions: emailBisonSetupInstructions('Warmup Disabled Receiving Bounces'), + extraFields: buildEmailBisonExtraFields('emailbison_warmup_disabled_receiving_bounces'), + }), + outputs: buildEmailBisonWarmupDisabledReceivingBouncesOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index b32941173ab..b1d747f529f 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -78,6 +78,25 @@ import { confluenceUserCreatedTrigger, confluenceWebhookTrigger, } from '@/triggers/confluence' +import { + emailBisonEmailAccountAddedTrigger, + emailBisonEmailAccountDisconnectedTrigger, + emailBisonEmailAccountReconnectedTrigger, + emailBisonEmailAccountRemovedTrigger, + emailBisonEmailBouncedTrigger, + emailBisonEmailOpenedTrigger, + emailBisonEmailSentTrigger, + emailBisonLeadFirstContactedTrigger, + emailBisonLeadInterestedTrigger, + emailBisonLeadRepliedTrigger, + emailBisonLeadUnsubscribedTrigger, + emailBisonManualEmailSentTrigger, + emailBisonTagAttachedTrigger, + emailBisonTagRemovedTrigger, + emailBisonUntrackedReplyReceivedTrigger, + emailBisonWarmupDisabledCausingBouncesTrigger, + emailBisonWarmupDisabledReceivingBouncesTrigger, +} from '@/triggers/emailbison' import { fathomNewMeetingTrigger, fathomWebhookTrigger } from '@/triggers/fathom' import { firefliesTranscriptionCompleteTrigger } from '@/triggers/fireflies' import { genericWebhookTrigger } from '@/triggers/generic' @@ -379,6 +398,23 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { confluence_space_removed: confluenceSpaceRemovedTrigger, confluence_page_permissions_updated: confluencePagePermissionsUpdatedTrigger, confluence_user_created: confluenceUserCreatedTrigger, + emailbison_email_sent: emailBisonEmailSentTrigger, + emailbison_lead_first_contacted: emailBisonLeadFirstContactedTrigger, + emailbison_lead_replied: emailBisonLeadRepliedTrigger, + emailbison_lead_interested: emailBisonLeadInterestedTrigger, + emailbison_lead_unsubscribed: emailBisonLeadUnsubscribedTrigger, + emailbison_untracked_reply_received: emailBisonUntrackedReplyReceivedTrigger, + emailbison_email_opened: emailBisonEmailOpenedTrigger, + emailbison_email_bounced: emailBisonEmailBouncedTrigger, + emailbison_email_account_added: emailBisonEmailAccountAddedTrigger, + emailbison_email_account_removed: emailBisonEmailAccountRemovedTrigger, + emailbison_email_account_disconnected: emailBisonEmailAccountDisconnectedTrigger, + emailbison_email_account_reconnected: emailBisonEmailAccountReconnectedTrigger, + emailbison_manual_email_sent: emailBisonManualEmailSentTrigger, + emailbison_tag_attached: emailBisonTagAttachedTrigger, + emailbison_tag_removed: emailBisonTagRemovedTrigger, + emailbison_warmup_disabled_receiving_bounces: emailBisonWarmupDisabledReceivingBouncesTrigger, + emailbison_warmup_disabled_causing_bounces: emailBisonWarmupDisabledCausingBouncesTrigger, generic_webhook: genericWebhookTrigger, greenhouse_candidate_hired: greenhouseCandidateHiredTrigger, greenhouse_new_application: greenhouseNewApplicationTrigger, From 369f9b613de42628d48c7435c31f17626059c4b6 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 6 May 2026 13:03:09 -0700 Subject: [PATCH 10/17] fix(office-excel): support Office.js add-in embed and surface Graph errors (#4479) * fix(office-excel): support Office.js add-in embed and surface Graph errors * fix(office-excel): delegate to parseGraphErrorFromData and handle array embed param --- .../api/tools/microsoft_excel/drives/route.ts | 20 +--- .../api/tools/microsoft_excel/sheets/route.ts | 14 +-- .../chat/[identifier]/office-embed-init.tsx | 50 +++++++++ apps/sim/app/chat/[identifier]/page.tsx | 19 +++- apps/sim/lib/core/security/csp.test.ts | 19 ++++ apps/sim/lib/core/security/csp.ts | 15 ++- apps/sim/tools/error-extractors.ts | 10 ++ apps/sim/tools/microsoft_excel/read.ts | 18 ++- apps/sim/tools/microsoft_excel/table_add.ts | 2 + apps/sim/tools/microsoft_excel/utils.test.ts | 90 +++++++++++++++ apps/sim/tools/microsoft_excel/utils.ts | 106 ++++++++++++++++++ .../tools/microsoft_excel/worksheet_add.ts | 15 +-- apps/sim/tools/microsoft_excel/write.ts | 18 +++ 13 files changed, 354 insertions(+), 42 deletions(-) create mode 100644 apps/sim/app/chat/[identifier]/office-embed-init.tsx create mode 100644 apps/sim/tools/microsoft_excel/utils.test.ts diff --git a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts index df884bea928..2e0d1d80e43 100644 --- a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts @@ -7,7 +7,7 @@ import { validatePathSegment, validateSharePointSiteId } from '@/lib/core/securi import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' -import { GRAPH_ID_PATTERN } from '@/tools/microsoft_excel/utils' +import { extractGraphError, GRAPH_ID_PATTERN } from '@/tools/microsoft_excel/utils' export const dynamic = 'force-dynamic' @@ -76,13 +76,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) if (!response.ok) { - const errorData = await response - .json() - .catch(() => ({ error: { message: 'Unknown error' } })) - return NextResponse.json( - { error: errorData.error?.message || 'Failed to fetch drive' }, - { status: response.status } - ) + const errorMessage = await extractGraphError(response) + return NextResponse.json({ error: errorMessage }, { status: response.status }) } const data: GraphDrive = await response.json() @@ -102,15 +97,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) if (!response.ok) { - const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } })) + const errorMessage = await extractGraphError(response) logger.error(`[${requestId}] Microsoft Graph API error fetching drives`, { status: response.status, - error: errorData.error?.message, + error: errorMessage, }) - return NextResponse.json( - { error: errorData.error?.message || 'Failed to fetch drives' }, - { status: response.status } - ) + return NextResponse.json({ error: errorMessage }, { status: response.status }) } const data = await response.json() diff --git a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts index 7a2c64cf6c3..19212aaa59a 100644 --- a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts @@ -6,7 +6,7 @@ import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' -import { getItemBasePath } from '@/tools/microsoft_excel/utils' +import { extractGraphError, getItemBasePath } from '@/tools/microsoft_excel/utils' export const dynamic = 'force-dynamic' @@ -73,18 +73,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => { }) if (!worksheetsResponse.ok) { - const errorData = await worksheetsResponse - .text() - .then((text) => JSON.parse(text)) - .catch(() => ({ error: { message: 'Unknown error' } })) + const errorMessage = await extractGraphError(worksheetsResponse) logger.error(`[${requestId}] Microsoft Graph API error`, { status: worksheetsResponse.status, - error: errorData.error?.message || 'Failed to fetch worksheets', + error: errorMessage, }) - return NextResponse.json( - { error: errorData.error?.message || 'Failed to fetch worksheets' }, - { status: worksheetsResponse.status } - ) + return NextResponse.json({ error: errorMessage }, { status: worksheetsResponse.status }) } const data: WorksheetsResponse = await worksheetsResponse.json() diff --git a/apps/sim/app/chat/[identifier]/office-embed-init.tsx b/apps/sim/app/chat/[identifier]/office-embed-init.tsx new file mode 100644 index 00000000000..02729d65274 --- /dev/null +++ b/apps/sim/app/chat/[identifier]/office-embed-init.tsx @@ -0,0 +1,50 @@ +'use client' + +import Script from 'next/script' + +declare global { + interface Window { + Office?: { + onReady: () => Promise<{ host: string | null; platform: string | null }> + } + } +} + +/** + * Office.js nullifies window.history.replaceState and pushState (a legacy + * IE10 workaround inside the library) which breaks Next.js's client-side + * router. Cache the originals at module load — before

Trello connection failed. Redirecting...

`, + { + status: 400, + headers: { + 'Content-Type': 'text/html; charset=utf-8', + 'Cache-Control': 'no-store, no-cache, must-revalidate', + }, + } + ) +} + export const GET = withRouteHandler(async (request: NextRequest) => { const parsed = await parseRequest(trelloCallbackContract, request, {}) if (!parsed.success) return parsed.response const baseUrl = getBaseUrl() + const queryState = parsed.data.query.state + const cookieState = request.cookies.get(TRELLO_STATE_COOKIE)?.value + + if (!queryState || !cookieState || queryState !== cookieState) { + logger.warn('Trello callback rejected: state mismatch or missing state', { + hasQueryState: Boolean(queryState), + hasCookieState: Boolean(cookieState), + }) + const response = renderErrorPage(baseUrl, 'error=trello_state_mismatch') + response.cookies.delete({ name: TRELLO_STATE_COOKIE, path: '/api/auth/trello' }) + return response + } + + const safeState = escapeForJsString(queryState) return new NextResponse( ` @@ -97,7 +135,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', - body: JSON.stringify({ token: token }) + body: JSON.stringify({ token: token, state: '${safeState}' }) }) .then(response => response.json()) .then(data => { diff --git a/apps/sim/app/api/auth/trello/store/route.ts b/apps/sim/app/api/auth/trello/store/route.ts index d62237e7015..156ed9a65d6 100644 --- a/apps/sim/app/api/auth/trello/store/route.ts +++ b/apps/sim/app/api/auth/trello/store/route.ts @@ -16,6 +16,14 @@ const logger = createLogger('TrelloStore') export const dynamic = 'force-dynamic' +const TRELLO_STATE_COOKIE = 'trello_oauth_state' +const TRELLO_STATE_COOKIE_PATH = '/api/auth/trello' + +function clearStateCookie(response: NextResponse) { + response.cookies.delete({ name: TRELLO_STATE_COOKIE, path: TRELLO_STATE_COOKIE_PATH }) + return response +} + export const POST = withRouteHandler(async (request: NextRequest) => { try { const session = await getSession() @@ -26,7 +34,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const parsed = await parseRequest(storeTrelloTokenContract, request, {}) if (!parsed.success) return parsed.response - const { token } = parsed.data.body + const { token, state } = parsed.data.body + + const cookieState = request.cookies.get(TRELLO_STATE_COOKIE)?.value + if (!cookieState || cookieState !== state) { + logger.warn('Trello store rejected: state mismatch', { + hasCookieState: Boolean(cookieState), + userId: session.user.id, + }) + return clearStateCookie( + NextResponse.json( + { success: false, error: 'Invalid or expired authorization state' }, + { status: 400 } + ) + ) + } const apiKey = env.TRELLO_API_KEY if (!apiKey) { @@ -123,7 +145,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } } - return NextResponse.json({ success: true }) + return clearStateCookie(NextResponse.json({ success: true })) } catch (error) { logger.error('Error storing Trello token:', error) return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 }) diff --git a/apps/sim/app/api/chat/manage/[id]/route.ts b/apps/sim/app/api/chat/manage/[id]/route.ts index 933636dd12b..8a70bcb9775 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.ts @@ -110,6 +110,10 @@ export const PATCH = withRouteHandler( outputConfigs, } = validatedData + if (workflowId && workflowId !== existingChat[0].workflowId) { + return createErrorResponse('Changing the workflow of a chat deployment is not allowed', 400) + } + if (identifier && identifier !== existingChat[0].identifier) { const existingIdentifier = await db .select() @@ -156,7 +160,6 @@ export const PATCH = withRouteHandler( updatedAt: new Date(), } - if (workflowId) updateData.workflowId = workflowId if (identifier) updateData.identifier = identifier if (title) updateData.title = title if (description !== undefined) updateData.description = description diff --git a/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts b/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts index ec1b0833ab2..31bf0d8fe07 100644 --- a/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts +++ b/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { dataverseUploadFileContract } from '@/lib/api/contracts/tools/microsoft' import { parseRequest } 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 { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' @@ -78,20 +79,26 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const baseUrl = validatedData.environmentUrl.replace(/\/$/, '') const uploadUrl = `${baseUrl}/api/data/v9.2/${validatedData.entitySetName}(${validatedData.recordId})/${validatedData.fileColumn}` - const response = await fetch(uploadUrl, { - method: 'PATCH', - headers: { - Authorization: `Bearer ${validatedData.accessToken}`, - 'Content-Type': 'application/octet-stream', - 'OData-MaxVersion': '4.0', - 'OData-Version': '4.0', - 'x-ms-file-name': validatedData.fileName, + const response = await secureFetchWithValidation( + uploadUrl, + { + method: 'PATCH', + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + 'Content-Type': 'application/octet-stream', + 'OData-MaxVersion': '4.0', + 'OData-Version': '4.0', + 'x-ms-file-name': validatedData.fileName, + }, + body: fileBuffer, }, - body: new Uint8Array(fileBuffer), - }) + 'environmentUrl' + ) if (!response.ok) { - const errorData = await response.json().catch(() => ({})) + const errorData = (await response.json().catch(() => ({}))) as { + error?: { message?: string } + } const errorMessage = errorData?.error?.message ?? `Dataverse API error: ${response.status} ${response.statusText}` diff --git a/apps/sim/app/api/v1/logs/[id]/route.ts b/apps/sim/app/api/v1/logs/[id]/route.ts index 81e876c25f5..c32acfd444c 100644 --- a/apps/sim/app/api/v1/logs/[id]/route.ts +++ b/apps/sim/app/api/v1/logs/[id]/route.ts @@ -1,14 +1,18 @@ import { db } from '@sim/db' -import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema' +import { workflow, workflowExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' -import { and, eq } from 'drizzle-orm' +import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { v1GetLogContract } from '@/lib/api/contracts/v1/logs' import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' -import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware' +import { + checkRateLimit, + createRateLimitResponse, + validateWorkspaceAccess, +} from '@/app/api/v1/middleware' const logger = createLogger('V1LogDetailsAPI') @@ -37,6 +41,7 @@ export const GET = withRouteHandler( .select({ id: workflowExecutionLogs.id, workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, executionId: workflowExecutionLogs.executionId, stateSnapshotId: workflowExecutionLogs.stateSnapshotId, level: workflowExecutionLogs.level, @@ -59,14 +64,6 @@ export const GET = withRouteHandler( }) .from(workflowExecutionLogs) .leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id)) - .innerJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workflowExecutionLogs.workspaceId), - eq(permissions.userId, userId) - ) - ) .where(eq(workflowExecutionLogs.id, id)) .limit(1) @@ -75,6 +72,11 @@ export const GET = withRouteHandler( return NextResponse.json({ error: 'Log not found' }, { status: 404 }) } + const accessError = await validateWorkspaceAccess(rateLimit, userId, log.workspaceId) + if (accessError) { + return NextResponse.json({ error: 'Log not found' }, { status: 404 }) + } + const workflowSummary = { id: log.workflowId, name: log.workflowName || 'Deleted Workflow', diff --git a/apps/sim/app/api/v1/logs/executions/[executionId]/route.ts b/apps/sim/app/api/v1/logs/executions/[executionId]/route.ts index 1f373c2e3f7..e7503ecb071 100644 --- a/apps/sim/app/api/v1/logs/executions/[executionId]/route.ts +++ b/apps/sim/app/api/v1/logs/executions/[executionId]/route.ts @@ -1,13 +1,17 @@ import { db } from '@sim/db' -import { permissions, workflowExecutionLogs, workflowExecutionSnapshots } from '@sim/db/schema' +import { workflowExecutionLogs, workflowExecutionSnapshots } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq } from 'drizzle-orm' +import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { v1GetExecutionContract } from '@/lib/api/contracts/v1/logs' import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' -import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware' +import { + checkRateLimit, + createRateLimitResponse, + validateWorkspaceAccess, +} from '@/app/api/v1/middleware' const logger = createLogger('V1ExecutionAPI') @@ -31,18 +35,8 @@ export const GET = withRouteHandler( logger.debug(`Fetching execution data for: ${executionId}`) const rows = await db - .select({ - log: workflowExecutionLogs, - }) + .select() .from(workflowExecutionLogs) - .innerJoin( - permissions, - and( - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workflowExecutionLogs.workspaceId), - eq(permissions.userId, userId) - ) - ) .where(eq(workflowExecutionLogs.executionId, executionId)) .limit(1) @@ -50,7 +44,12 @@ export const GET = withRouteHandler( return NextResponse.json({ error: 'Workflow execution not found' }, { status: 404 }) } - const { log: workflowLog } = rows[0] + const workflowLog = rows[0] + + const accessError = await validateWorkspaceAccess(rateLimit, userId, workflowLog.workspaceId) + if (accessError) { + return NextResponse.json({ error: 'Workflow execution not found' }, { status: 404 }) + } const [snapshot] = await db .select() diff --git a/apps/sim/app/api/v1/logs/route.ts b/apps/sim/app/api/v1/logs/route.ts index 53513407c60..0f8f7b31b82 100644 --- a/apps/sim/app/api/v1/logs/route.ts +++ b/apps/sim/app/api/v1/logs/route.ts @@ -9,7 +9,11 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { buildLogFilters, getOrderBy } from '@/app/api/v1/logs/filters' import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' -import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware' +import { + checkRateLimit, + checkWorkspaceScope, + createRateLimitResponse, +} from '@/app/api/v1/middleware' const logger = createLogger('V1LogsAPI') @@ -62,6 +66,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => { const params = parsed.data.query + const scopeError = checkWorkspaceScope(rateLimit, params.workspaceId) + if (scopeError) return scopeError + logger.info(`[${requestId}] Fetching logs for workspace ${params.workspaceId}`, { userId, filters: { diff --git a/apps/sim/app/api/v1/workflows/[id]/route.ts b/apps/sim/app/api/v1/workflows/[id]/route.ts index 309eb3b96de..76d7393870f 100644 --- a/apps/sim/app/api/v1/workflows/[id]/route.ts +++ b/apps/sim/app/api/v1/workflows/[id]/route.ts @@ -9,9 +9,12 @@ import { v1GetWorkflowContract } from '@/lib/api/contracts/v1/workflows' import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' -import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' -import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware' +import { + checkRateLimit, + createRateLimitResponse, + validateWorkspaceAccess, +} from '@/app/api/v1/middleware' const logger = createLogger('V1WorkflowDetailsAPI') @@ -43,13 +46,13 @@ export const GET = withRouteHandler( return NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) } - const permission = await getUserEntityPermissions( + const accessError = await validateWorkspaceAccess( + rateLimit, userId, - 'workspace', workflowData.workspaceId! ) - if (!permission) { - return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + if (accessError) { + return NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) } const blockRows = await db diff --git a/apps/sim/app/api/v1/workflows/route.ts b/apps/sim/app/api/v1/workflows/route.ts index cbb3e7ffc45..707a6a3de43 100644 --- a/apps/sim/app/api/v1/workflows/route.ts +++ b/apps/sim/app/api/v1/workflows/route.ts @@ -7,9 +7,12 @@ import { type NextRequest, NextResponse } from 'next/server' import { v1ListWorkflowsContract } from '@/lib/api/contracts/v1/workflows' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' -import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware' +import { + checkRateLimit, + createRateLimitResponse, + validateWorkspaceAccess, +} from '@/app/api/v1/middleware' const logger = createLogger('V1WorkflowsAPI') @@ -71,10 +74,8 @@ export const GET = withRouteHandler(async (request: NextRequest) => { }, }) - const permission = await getUserEntityPermissions(userId, 'workspace', params.workspaceId) - if (!permission) { - return NextResponse.json({ error: 'Access denied' }, { status: 403 }) - } + const accessError = await validateWorkspaceAccess(rateLimit, userId, params.workspaceId) + if (accessError) return accessError const conditions = [eq(workflow.workspaceId, params.workspaceId), isNull(workflow.archivedAt)] diff --git a/apps/sim/lib/api/contracts/oauth-connections.ts b/apps/sim/lib/api/contracts/oauth-connections.ts index cc778ee6abb..03ec64d0a66 100644 --- a/apps/sim/lib/api/contracts/oauth-connections.ts +++ b/apps/sim/lib/api/contracts/oauth-connections.ts @@ -43,10 +43,18 @@ export type ConnectedAccount = z.output export const trelloTokenBodySchema = z.object({ token: z.string().min(1), + state: z.string().min(1, 'state is required'), }) const emptyTrelloAuthQuerySchema = z.object({}).passthrough() +const trelloCallbackQuerySchema = z + .object({ + state: z.string().min(1).optional(), + error: z.string().min(1).optional(), + }) + .passthrough() + export const oauthTokenRequestBodySchema = z .object({ credentialId: z.string().min(1).optional(), @@ -203,7 +211,7 @@ export const authorizeTrelloContract = defineRouteContract({ export const trelloCallbackContract = defineRouteContract({ method: 'GET', path: '/api/auth/trello/callback', - query: emptyTrelloAuthQuerySchema, + query: trelloCallbackQuerySchema, response: { mode: 'text' }, }) diff --git a/apps/sim/lib/webhooks/providers/stripe.ts b/apps/sim/lib/webhooks/providers/stripe.ts index 7b9414ab788..449844042c8 100644 --- a/apps/sim/lib/webhooks/providers/stripe.ts +++ b/apps/sim/lib/webhooks/providers/stripe.ts @@ -1,5 +1,8 @@ import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import Stripe from 'stripe' import type { + AuthContext, EventFilterContext, FormatInputContext, FormatInputResult, @@ -10,6 +13,33 @@ import { skipByEventTypes } from '@/lib/webhooks/providers/utils' const logger = createLogger('WebhookProvider:Stripe') export const stripeHandler: WebhookProviderHandler = { + verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) { + const secret = providerConfig.webhookSecret as string | undefined + if (!secret) { + logger.warn( + `[${requestId}] Stripe webhook missing webhookSecret in providerConfig — rejecting request` + ) + return new NextResponse('Unauthorized - Webhook secret not configured', { status: 401 }) + } + + const signature = request.headers.get('stripe-signature') + if (!signature) { + logger.warn(`[${requestId}] Stripe webhook missing Stripe-Signature header`) + return new NextResponse('Unauthorized - Missing Stripe signature', { status: 401 }) + } + + try { + Stripe.webhooks.constructEvent(rawBody, signature, secret) + } catch (error) { + logger.warn(`[${requestId}] Stripe signature verification failed`, { + error: error instanceof Error ? error.message : String(error), + }) + return new NextResponse('Unauthorized - Invalid Stripe signature', { status: 401 }) + } + + return null + }, + async formatInput({ body }: FormatInputContext): Promise { return { input: body } }, diff --git a/apps/sim/package.json b/apps/sim/package.json index 9eadf53bcd4..2304b992c53 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -62,7 +62,7 @@ "@hookform/resolvers": "5.2.2", "@linear/sdk": "40.0.0", "@marsidev/react-turnstile": "1.4.2", - "@modelcontextprotocol/sdk": "1.25.3", + "@modelcontextprotocol/sdk": "1.29.0", "@monaco-editor/react": "4.7.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-jaeger": "2.1.0", @@ -147,7 +147,7 @@ "isolated-vm": "6.0.2", "jose": "6.0.11", "js-tiktoken": "1.0.21", - "js-yaml": "4.1.0", + "js-yaml": "4.1.1", "json5": "2.2.3", "jszip": "3.10.1", "jwt-decode": "^4.0.0", @@ -164,7 +164,7 @@ "next-mdx-remote": "^5.0.0", "next-runtime-env": "3.3.0", "next-themes": "^0.4.6", - "nodemailer": "7.0.11", + "nodemailer": "8.0.7", "officeparser": "^5.2.0", "openai": "^4.91.1", "papaparse": "5.5.3", @@ -206,7 +206,7 @@ "twilio": "5.9.0", "unpdf": "1.4.0", "uuid": "^11.1.0", - "xlsx": "0.18.5", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zod": "4.3.6", "zustand": "^4.5.7" }, diff --git a/bun.lock b/bun.lock index ecaf2a67bbf..0e2c363e7f0 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "simstudio", @@ -11,6 +12,7 @@ "json-schema-to-typescript": "15.0.4", "lint-staged": "16.0.0", "turbo": "2.9.6", + "yaml": "^2.8.1", }, }, "apps/docs": { @@ -115,7 +117,7 @@ "@hookform/resolvers": "5.2.2", "@linear/sdk": "40.0.0", "@marsidev/react-turnstile": "1.4.2", - "@modelcontextprotocol/sdk": "1.25.3", + "@modelcontextprotocol/sdk": "1.29.0", "@monaco-editor/react": "4.7.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-jaeger": "2.1.0", @@ -200,7 +202,7 @@ "isolated-vm": "6.0.2", "jose": "6.0.11", "js-tiktoken": "1.0.21", - "js-yaml": "4.1.0", + "js-yaml": "4.1.1", "json5": "2.2.3", "jszip": "3.10.1", "jwt-decode": "^4.0.0", @@ -217,7 +219,7 @@ "next-mdx-remote": "^5.0.0", "next-runtime-env": "3.3.0", "next-themes": "^0.4.6", - "nodemailer": "7.0.11", + "nodemailer": "8.0.7", "officeparser": "^5.2.0", "openai": "^4.91.1", "papaparse": "5.5.3", @@ -259,7 +261,7 @@ "twilio": "5.9.0", "unpdf": "1.4.0", "uuid": "^11.1.0", - "xlsx": "0.18.5", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zod": "4.3.6", "zustand": "^4.5.7", }, @@ -475,6 +477,14 @@ "reactflow": "^11.11.4", }, }, + "scripts": { + "name": "sim-doc-generator", + "version": "1.0.0", + "dependencies": { + "glob": "^11.1.0", + "yaml": "^2.8.1", + }, + }, }, "trustedDependencies": [ "ffmpeg-static", @@ -484,6 +494,7 @@ "overrides": { "@next/env": "16.2.4", "drizzle-orm": "^0.45.2", + "minimatch": "^10.2.5", "next": "16.2.4", "postgres": "^3.4.5", "react": "19.2.4", @@ -1018,7 +1029,7 @@ "@microsoft/fetch-event-source": ["@microsoft/fetch-event-source@2.0.1", "", {}, "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], @@ -1890,8 +1901,6 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "adler-32": ["adler-32@1.3.1", "", {}, "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], @@ -2058,8 +2067,6 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - "cfb": ["cfb@1.2.2", "", { "dependencies": { "adler-32": "~1.3.0", "crc-32": "~1.2.0" } }, "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA=="], - "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -2122,8 +2129,6 @@ "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], - "codepage": ["codepage@1.15.0", "", {}, "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="], - "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], @@ -2176,8 +2181,6 @@ "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], - "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], - "critters": ["critters@0.0.25", "", { "dependencies": { "chalk": "^4.1.0", "css-select": "^5.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.2", "htmlparser2": "^8.0.2", "postcss": "^8.4.23", "postcss-media-query-parser": "^0.2.3" } }, "sha512-ROF/tjJyyRdM8/6W0VqoN5Ql05xAGnkf5b7f3sTEl1bI5jTQQf8O918RD/V9tEb9pRY/TKcvJekDbJtniHyPtQ=="], "croner": ["croner@9.1.0", "", {}, "sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g=="], @@ -2504,7 +2507,7 @@ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + "express-rate-limit": ["express-rate-limit@8.4.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], @@ -2576,8 +2579,6 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="], - "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], @@ -2760,7 +2761,7 @@ "ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="], - "ip-address": ["ip-address@10.1.1", "", {}, "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], @@ -2842,7 +2843,7 @@ "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], @@ -3236,7 +3237,7 @@ "node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="], - "nodemailer": ["nodemailer@7.0.11", "", {}, "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw=="], + "nodemailer": ["nodemailer@8.0.7", "", {}, "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -3696,6 +3697,8 @@ "sim": ["sim@workspace:apps/sim"], + "sim-doc-generator": ["sim-doc-generator@workspace:scripts"], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], @@ -3748,8 +3751,6 @@ "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], - "ssf": ["ssf@0.11.2", "", { "dependencies": { "frac": "~1.1.2" } }, "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="], - "ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -4050,10 +4051,6 @@ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "wmf": ["wmf@1.0.2", "", {}, "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="], - - "word": ["word@0.3.0", "", {}, "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="], - "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -4062,7 +4059,7 @@ "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], - "xlsx": ["xlsx@0.18.5", "", { "dependencies": { "adler-32": "~1.3.0", "cfb": "~1.2.1", "codepage": "~1.15.0", "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" }, "bin": { "xlsx": "bin/xlsx.njs" } }, "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="], + "xlsx": ["xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", { "bin": { "xlsx": "./bin/xlsx.njs" } }, "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA=="], "xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="], @@ -4118,8 +4115,6 @@ "@antfu/install-pkg/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], - "@apidevtools/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@authenio/xml-encryption/xpath": ["xpath@0.0.32", "", {}, "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw=="], @@ -4396,6 +4391,8 @@ "@browserbasehq/stagehand/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="], + "@browserbasehq/stagehand/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], + "@cerebras/cerebras_cloud_sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "@cerebras/cerebras_cloud_sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], @@ -4412,6 +4409,8 @@ "@langchain/openai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], "@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], @@ -4706,22 +4705,16 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "fumadocs-core/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "fumadocs-core/shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="], "fumadocs-mdx/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], - "fumadocs-mdx/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "fumadocs-mdx/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], "fumadocs-openapi/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], "fumadocs-openapi/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "fumadocs-openapi/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "fumadocs-openapi/lucide-react": ["lucide-react@1.14.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA=="], "fumadocs-openapi/shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="], @@ -4762,6 +4755,8 @@ "jaeger-client/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "json-schema-to-typescript/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "langsmith/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -4894,6 +4889,8 @@ "sim/tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], + "sim-doc-generator/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "simstudio/@types/node": ["@types/node@20.19.39", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw=="], "simstudio/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -4908,6 +4905,8 @@ "socket.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + "socks/ip-address": ["ip-address@10.1.1", "", {}, "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -5098,6 +5097,10 @@ "@browserbasehq/stagehand/@anthropic-ai/sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "@browserbasehq/stagehand/@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + + "@browserbasehq/stagehand/@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + "@cerebras/cerebras_cloud_sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], @@ -5500,8 +5503,6 @@ "test-exclude/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - "test-exclude/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "test-exclude/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], @@ -5762,8 +5763,6 @@ "test-exclude/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - "test-exclude/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], - "test-exclude/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@aws-sdk/client-sqs/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.947.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.947.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.947.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.947.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.7", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.14", "@smithy/middleware-retry": "^4.4.14", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.10", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.13", "@smithy/util-defaults-mode-node": "^4.2.16", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DjRJEYNnHUTu9kGPPQDTSXquwSEd6myKR4ssI4FaYLFhdT3ldWpj73yYt807H3tdmhS7vPmdVqchSJnjurUQAw=="], @@ -5798,8 +5797,6 @@ "test-exclude/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - "test-exclude/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "lint-staged/listr2/cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "lint-staged/listr2/log-update/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], diff --git a/package.json b/package.json index eea53bc1299..21d2cc7b80a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "license": "Apache-2.0", "workspaces": [ "apps/*", - "packages/*" + "packages/*", + "scripts" ], "scripts": { "build": "turbo run build", @@ -51,7 +52,8 @@ "next": "16.2.4", "@next/env": "16.2.4", "drizzle-orm": "^0.45.2", - "postgres": "^3.4.5" + "postgres": "^3.4.5", + "minimatch": "^10.2.5" }, "devDependencies": { "@biomejs/biome": "2.0.0-beta.5", @@ -60,7 +62,8 @@ "husky": "9.1.7", "json-schema-to-typescript": "15.0.4", "lint-staged": "16.0.0", - "turbo": "2.9.6" + "turbo": "2.9.6", + "yaml": "^2.8.1" }, "lint-staged": { "*.{js,jsx,ts,tsx,json,css,scss}": [ diff --git a/scripts/README.md b/scripts/README.md index 0c0f1394115..925345284d9 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -4,8 +4,7 @@ This directory contains scripts to automatically generate documentation for all ## Available Scripts -- `generate-docs.sh`: Generates documentation for all blocks -- `setup-doc-generator.sh`: Installs dependencies required for the documentation generator +- `generate-docs.ts`: Generates documentation for all blocks. Run via `bun run generate-docs` from `apps/sim`, or directly with `bun run scripts/generate-docs.ts` from the repo root. ## How It Works @@ -21,32 +20,12 @@ The documentation generator: ## Running the Generator -To generate documentation manually: - ```bash -# From the project root -./scripts/generate-docs.sh +# From the repo root +bun run scripts/generate-docs.ts ``` -## Troubleshooting TypeScript Errors - -If you encounter TypeScript errors when running the documentation generator, run the setup script to install the necessary dependencies: - -```bash -./scripts/setup-doc-generator.sh -``` - -This will: - -1. Install TypeScript, ts-node, and necessary type definitions -2. Create a proper tsconfig.json for the scripts directory -3. Configure the scripts directory to use ES modules - -### Common Issues - -1. **Missing Type Declarations**: Run the setup script to install @types/node and @types/react -2. **JSX Errors in block-info-card.tsx**: These don't affect functionality and can be ignored if you've run the setup script -3. **Module Resolution**: The setup script configures proper ES module support +Dependencies are managed by Bun workspaces — `bun install` at the repo root installs everything needed. ## CI Integration diff --git a/scripts/bun.lock b/scripts/bun.lock deleted file mode 100644 index 6888ce2311e..00000000000 --- a/scripts/bun.lock +++ /dev/null @@ -1,204 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "sim-doc-generator", - "dependencies": { - "@types/node": "^24.5.1", - "@types/react": "^19.1.13", - "glob": "^11.0.3", - "ts-node": "^10.9.2", - "tsx": "^4.20.5", - "typescript": "^5.9.2", - "yaml": "^2.8.1", - }, - }, - }, - "packages": { - "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.2", "", { "os": "android", "cpu": "arm" }, "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.2", "", { "os": "android", "cpu": "x64" }, "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.2", "", { "os": "linux", "cpu": "arm" }, "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.2", "", { "os": "linux", "cpu": "x64" }, "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.2", "", { "os": "none", "cpu": "arm64" }, "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.2", "", { "os": "none", "cpu": "x64" }, "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.2", "", { "os": "win32", "cpu": "x64" }, "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="], - - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], - - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], - - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - - "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="], - - "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], - - "@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="], - - "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="], - - "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - - "@types/react": ["@types/react@19.2.3", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg=="], - - "acorn": ["acorn@8.14.1", "", { "bin": "bin/acorn" }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], - - "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], - - "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - - "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], - - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - - "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "esbuild": ["esbuild@0.25.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.2", "@esbuild/android-arm": "0.25.2", "@esbuild/android-arm64": "0.25.2", "@esbuild/android-x64": "0.25.2", "@esbuild/darwin-arm64": "0.25.2", "@esbuild/darwin-x64": "0.25.2", "@esbuild/freebsd-arm64": "0.25.2", "@esbuild/freebsd-x64": "0.25.2", "@esbuild/linux-arm": "0.25.2", "@esbuild/linux-arm64": "0.25.2", "@esbuild/linux-ia32": "0.25.2", "@esbuild/linux-loong64": "0.25.2", "@esbuild/linux-mips64el": "0.25.2", "@esbuild/linux-ppc64": "0.25.2", "@esbuild/linux-riscv64": "0.25.2", "@esbuild/linux-s390x": "0.25.2", "@esbuild/linux-x64": "0.25.2", "@esbuild/netbsd-arm64": "0.25.2", "@esbuild/netbsd-x64": "0.25.2", "@esbuild/openbsd-arm64": "0.25.2", "@esbuild/openbsd-x64": "0.25.2", "@esbuild/sunos-x64": "0.25.2", "@esbuild/win32-arm64": "0.25.2", "@esbuild/win32-ia32": "0.25.2", "@esbuild/win32-x64": "0.25.2" }, "bin": "bin/esbuild" }, "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ=="], - - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], - - "glob": ["glob@11.0.3", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], - - "lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="], - - "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], - - "minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], - - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="], - - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js", "ts-script": "dist/bin-script-deprecated.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], - - "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], - - "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], - - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - } -} diff --git a/scripts/package-lock.json b/scripts/package-lock.json deleted file mode 100644 index 601299d2a72..00000000000 --- a/scripts/package-lock.json +++ /dev/null @@ -1,1287 +0,0 @@ -{ - "name": "sim-doc-generator", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "sim-doc-generator", - "version": "1.0.0", - "devDependencies": { - "@types/node": "^22.15.17", - "@types/react": "^19.1.3", - "glob": "^11.0.2", - "ts-node": "^10.9.2", - "tsx": "^4.19.4", - "typescript": "^5.8.3" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.15.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", - "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/react": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", - "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.2", - "@esbuild/android-arm": "0.25.2", - "@esbuild/android-arm64": "0.25.2", - "@esbuild/android-x64": "0.25.2", - "@esbuild/darwin-arm64": "0.25.2", - "@esbuild/darwin-x64": "0.25.2", - "@esbuild/freebsd-arm64": "0.25.2", - "@esbuild/freebsd-x64": "0.25.2", - "@esbuild/linux-arm": "0.25.2", - "@esbuild/linux-arm64": "0.25.2", - "@esbuild/linux-ia32": "0.25.2", - "@esbuild/linux-loong64": "0.25.2", - "@esbuild/linux-mips64el": "0.25.2", - "@esbuild/linux-ppc64": "0.25.2", - "@esbuild/linux-riscv64": "0.25.2", - "@esbuild/linux-s390x": "0.25.2", - "@esbuild/linux-x64": "0.25.2", - "@esbuild/netbsd-arm64": "0.25.2", - "@esbuild/netbsd-x64": "0.25.2", - "@esbuild/openbsd-arm64": "0.25.2", - "@esbuild/openbsd-x64": "0.25.2", - "@esbuild/sunos-x64": "0.25.2", - "@esbuild/win32-arm64": "0.25.2", - "@esbuild/win32-ia32": "0.25.2", - "@esbuild/win32-x64": "0.25.2" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", - "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", - "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/tsx": { - "version": "4.19.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", - "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - } - } -} diff --git a/scripts/package.json b/scripts/package.json index f571e7b0c39..e2d16dfbdb2 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,16 +1,11 @@ { "name": "sim-doc-generator", "version": "1.0.0", - "description": "Documentation generator for Sim blocks", + "description": "Documentation generator and contract sync scripts for Sim", "type": "module", "private": true, "dependencies": { - "@types/node": "^24.5.1", - "@types/react": "^19.1.13", - "glob": "^11.0.3", - "ts-node": "^10.9.2", - "tsx": "^4.20.5", - "typescript": "^5.9.2", + "glob": "^11.1.0", "yaml": "^2.8.1" } } diff --git a/scripts/setup-doc-generator.sh b/scripts/setup-doc-generator.sh deleted file mode 100755 index fcdf4741d34..00000000000 --- a/scripts/setup-doc-generator.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -# Get the scripts directory path -SCRIPTS_DIR=$(dirname "$0") -cd "$SCRIPTS_DIR" -echo "Working in scripts directory: $(pwd)" - -echo "Setting up documentation generator..." - -# Create package.json for scripts directory -cat > package.json << EOF -{ - "name": "sim-doc-generator", - "version": "1.0.0", - "description": "Documentation generator for Sim blocks", - "type": "module", - "private": true -} -EOF - -# Install dependencies local to scripts directory -bun install --save-dev typescript @types/node @types/react ts-node tsx glob - -# Setup tsconfig.json -cat > tsconfig.json << EOF -{ - "compilerOptions": { - "target": "ES2020", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "noEmit": true, - "allowImportingTsExtensions": true - }, - "ts-node": { - "esm": true, - "experimentalSpecifierResolution": "node" - }, - "include": ["./**/*.ts"] -} -EOF - -echo "Dependencies installed successfully!" -echo "You can now run './scripts/generate-docs.sh' to generate the documentation." \ No newline at end of file From a251e45400891719da1b33a9407ae777ddb70b09 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 6 May 2026 19:32:27 -0700 Subject: [PATCH 14/17] feat(sap): add SAP Concur integration block and SAP S/4HANA validation fixes (#4483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 --------- Co-authored-by: Claude Opus 4.7 --- apps/docs/components/icons.tsx | 19 + apps/docs/components/ui/icon-mapping.ts | 2 + apps/docs/content/docs/en/tools/meta.json | 1 + .../docs/content/docs/en/tools/sap_concur.mdx | 2761 +++++++++++++++++ .../docs/content/docs/en/tools/sap_s4hana.mdx | 1005 ++++-- .../docs/content/docs/en/tools/sharepoint.mdx | 10 + .../integrations/data/icon-mapping.ts | 2 + .../integrations/data/integrations.json | 315 +- .../app/api/tools/sap_concur/proxy/route.ts | 133 + apps/sim/app/api/tools/sap_concur/shared.ts | 305 ++ .../app/api/tools/sap_concur/upload/route.ts | 279 ++ .../app/api/tools/sap_s4hana/proxy/route.ts | 73 +- apps/sim/blocks/blocks/sap_concur.ts | 1901 ++++++++++++ apps/sim/blocks/blocks/sap_s4hana.ts | 39 +- apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 19 + .../core/security/input-validation.server.ts | 27 +- apps/sim/tools/registry.ts | 142 + .../sap_concur/approve_expense_report.ts | 95 + .../tools/sap_concur/associate_attendees.ts | 123 + .../tools/sap_concur/create_cash_advance.ts | 90 + .../sap_concur/create_expected_expense.ts | 179 ++ .../tools/sap_concur/create_expense_report.ts | 109 + apps/sim/tools/sap_concur/create_list_item.ts | 121 + .../sap_concur/create_purchase_request.ts | 115 + .../tools/sap_concur/create_quick_expense.ts | 110 + .../create_quick_expense_with_image.ts | 123 + .../tools/sap_concur/create_report_comment.ts | 119 + .../tools/sap_concur/create_travel_request.ts | 264 ++ apps/sim/tools/sap_concur/create_user.ts | 85 + .../sap_concur/delete_expected_expense.ts | 100 + apps/sim/tools/sap_concur/delete_expense.ts | 96 + .../tools/sap_concur/delete_expense_report.ts | 86 + apps/sim/tools/sap_concur/delete_list_item.ts | 88 + .../tools/sap_concur/delete_travel_request.ts | 99 + apps/sim/tools/sap_concur/delete_user.ts | 87 + apps/sim/tools/sap_concur/get_allocation.ts | 156 + apps/sim/tools/sap_concur/get_budget.ts | 179 ++ apps/sim/tools/sap_concur/get_cash_advance.ts | 191 ++ .../tools/sap_concur/get_expected_expense.ts | 167 + apps/sim/tools/sap_concur/get_expense.ts | 357 +++ .../tools/sap_concur/get_expense_report.ts | 292 ++ apps/sim/tools/sap_concur/get_itemizations.ts | 174 ++ apps/sim/tools/sap_concur/get_itinerary.ts | 230 ++ apps/sim/tools/sap_concur/get_list.ts | 134 + apps/sim/tools/sap_concur/get_list_item.ts | 123 + .../tools/sap_concur/get_purchase_request.ts | 155 + apps/sim/tools/sap_concur/get_receipt.ts | 120 + .../tools/sap_concur/get_receipt_status.ts | 106 + .../sap_concur/get_request_cash_advance.ts | 130 + .../tools/sap_concur/get_travel_profile.ts | 234 ++ .../tools/sap_concur/get_travel_request.ts | 336 ++ apps/sim/tools/sap_concur/get_user.ts | 105 + apps/sim/tools/sap_concur/index.ts | 70 + .../tools/sap_concur/issue_cash_advance.ts | 109 + apps/sim/tools/sap_concur/list_allocations.ts | 117 + .../sap_concur/list_attendee_associations.ts | 193 ++ .../sap_concur/list_budget_categories.ts | 106 + apps/sim/tools/sap_concur/list_budgets.ts | 112 + apps/sim/tools/sap_concur/list_exceptions.ts | 131 + .../sap_concur/list_expected_expenses.ts | 100 + .../tools/sap_concur/list_expense_reports.ts | 275 ++ apps/sim/tools/sap_concur/list_expenses.ts | 216 ++ apps/sim/tools/sap_concur/list_itineraries.ts | 232 ++ apps/sim/tools/sap_concur/list_list_items.ts | 220 ++ apps/sim/tools/sap_concur/list_lists.ts | 209 ++ apps/sim/tools/sap_concur/list_receipts.ts | 107 + .../tools/sap_concur/list_report_comments.ts | 154 + .../sap_concur/list_reports_to_approve.ts | 175 ++ .../list_travel_profiles_summary.ts | 200 ++ .../list_travel_request_comments.ts | 118 + .../tools/sap_concur/list_travel_requests.ts | 349 +++ apps/sim/tools/sap_concur/list_users.ts | 109 + .../tools/sap_concur/move_travel_request.ts | 151 + .../tools/sap_concur/recall_expense_report.ts | 109 + .../tools/sap_concur/remove_all_attendees.ts | 110 + apps/sim/tools/sap_concur/search_locations.ts | 227 ++ apps/sim/tools/sap_concur/search_users.ts | 87 + .../sap_concur/send_back_expense_report.ts | 95 + .../tools/sap_concur/submit_expense_report.ts | 102 + apps/sim/tools/sap_concur/types.ts | 530 ++++ .../sim/tools/sap_concur/update_allocation.ts | 116 + .../sap_concur/update_expected_expense.ts | 177 ++ apps/sim/tools/sap_concur/update_expense.ts | 104 + .../tools/sap_concur/update_expense_report.ts | 109 + apps/sim/tools/sap_concur/update_list_item.ts | 131 + .../tools/sap_concur/update_travel_request.ts | 231 ++ apps/sim/tools/sap_concur/update_user.ts | 95 + .../tools/sap_concur/upload_exchange_rates.ts | 106 + .../tools/sap_concur/upload_receipt_image.ts | 119 + apps/sim/tools/sap_concur/utils.ts | 297 ++ .../sap_s4hana/create_business_partner.ts | 68 +- .../tools/sap_s4hana/create_purchase_order.ts | 47 +- .../sap_s4hana/create_purchase_requisition.ts | 46 +- .../tools/sap_s4hana/create_sales_order.ts | 44 +- .../tools/sap_s4hana/delete_sales_order.ts | 14 +- .../tools/sap_s4hana/get_billing_document.ts | 160 +- .../tools/sap_s4hana/get_business_partner.ts | 80 +- apps/sim/tools/sap_s4hana/get_customer.ts | 47 +- .../tools/sap_s4hana/get_inbound_delivery.ts | 89 +- .../tools/sap_s4hana/get_material_document.ts | 66 +- .../tools/sap_s4hana/get_outbound_delivery.ts | 103 +- apps/sim/tools/sap_s4hana/get_product.ts | 107 +- .../tools/sap_s4hana/get_purchase_order.ts | 84 +- .../sap_s4hana/get_purchase_requisition.ts | 44 +- apps/sim/tools/sap_s4hana/get_sales_order.ts | 80 +- apps/sim/tools/sap_s4hana/get_supplier.ts | 170 +- .../tools/sap_s4hana/get_supplier_invoice.ts | 87 +- .../sap_s4hana/list_billing_documents.ts | 176 +- .../sap_s4hana/list_business_partners.ts | 75 +- apps/sim/tools/sap_s4hana/list_customers.ts | 51 +- .../sap_s4hana/list_inbound_deliveries.ts | 105 +- .../sap_s4hana/list_material_documents.ts | 68 +- .../tools/sap_s4hana/list_material_stock.ts | 56 +- .../sap_s4hana/list_outbound_deliveries.ts | 115 +- apps/sim/tools/sap_s4hana/list_products.ts | 127 +- .../tools/sap_s4hana/list_purchase_orders.ts | 77 +- .../sap_s4hana/list_purchase_requisitions.ts | 53 +- .../sim/tools/sap_s4hana/list_sales_orders.ts | 79 +- .../sap_s4hana/list_supplier_invoices.ts | 103 +- apps/sim/tools/sap_s4hana/list_suppliers.ts | 188 +- apps/sim/tools/sap_s4hana/odata_query.ts | 10 +- .../sap_s4hana/update_business_partner.ts | 49 +- apps/sim/tools/sap_s4hana/update_customer.ts | 30 +- apps/sim/tools/sap_s4hana/update_product.ts | 36 +- .../tools/sap_s4hana/update_purchase_order.ts | 48 +- .../sap_s4hana/update_purchase_requisition.ts | 40 +- .../tools/sap_s4hana/update_sales_order.ts | 48 +- apps/sim/tools/sap_s4hana/update_supplier.ts | 58 +- scripts/check-api-validation-contracts.ts | 4 +- scripts/generate-docs.ts | 10 +- 131 files changed, 20997 insertions(+), 456 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/sap_concur.mdx create mode 100644 apps/sim/app/api/tools/sap_concur/proxy/route.ts create mode 100644 apps/sim/app/api/tools/sap_concur/shared.ts create mode 100644 apps/sim/app/api/tools/sap_concur/upload/route.ts create mode 100644 apps/sim/blocks/blocks/sap_concur.ts create mode 100644 apps/sim/tools/sap_concur/approve_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/associate_attendees.ts create mode 100644 apps/sim/tools/sap_concur/create_cash_advance.ts create mode 100644 apps/sim/tools/sap_concur/create_expected_expense.ts create mode 100644 apps/sim/tools/sap_concur/create_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/create_list_item.ts create mode 100644 apps/sim/tools/sap_concur/create_purchase_request.ts create mode 100644 apps/sim/tools/sap_concur/create_quick_expense.ts create mode 100644 apps/sim/tools/sap_concur/create_quick_expense_with_image.ts create mode 100644 apps/sim/tools/sap_concur/create_report_comment.ts create mode 100644 apps/sim/tools/sap_concur/create_travel_request.ts create mode 100644 apps/sim/tools/sap_concur/create_user.ts create mode 100644 apps/sim/tools/sap_concur/delete_expected_expense.ts create mode 100644 apps/sim/tools/sap_concur/delete_expense.ts create mode 100644 apps/sim/tools/sap_concur/delete_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/delete_list_item.ts create mode 100644 apps/sim/tools/sap_concur/delete_travel_request.ts create mode 100644 apps/sim/tools/sap_concur/delete_user.ts create mode 100644 apps/sim/tools/sap_concur/get_allocation.ts create mode 100644 apps/sim/tools/sap_concur/get_budget.ts create mode 100644 apps/sim/tools/sap_concur/get_cash_advance.ts create mode 100644 apps/sim/tools/sap_concur/get_expected_expense.ts create mode 100644 apps/sim/tools/sap_concur/get_expense.ts create mode 100644 apps/sim/tools/sap_concur/get_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/get_itemizations.ts create mode 100644 apps/sim/tools/sap_concur/get_itinerary.ts create mode 100644 apps/sim/tools/sap_concur/get_list.ts create mode 100644 apps/sim/tools/sap_concur/get_list_item.ts create mode 100644 apps/sim/tools/sap_concur/get_purchase_request.ts create mode 100644 apps/sim/tools/sap_concur/get_receipt.ts create mode 100644 apps/sim/tools/sap_concur/get_receipt_status.ts create mode 100644 apps/sim/tools/sap_concur/get_request_cash_advance.ts create mode 100644 apps/sim/tools/sap_concur/get_travel_profile.ts create mode 100644 apps/sim/tools/sap_concur/get_travel_request.ts create mode 100644 apps/sim/tools/sap_concur/get_user.ts create mode 100644 apps/sim/tools/sap_concur/index.ts create mode 100644 apps/sim/tools/sap_concur/issue_cash_advance.ts create mode 100644 apps/sim/tools/sap_concur/list_allocations.ts create mode 100644 apps/sim/tools/sap_concur/list_attendee_associations.ts create mode 100644 apps/sim/tools/sap_concur/list_budget_categories.ts create mode 100644 apps/sim/tools/sap_concur/list_budgets.ts create mode 100644 apps/sim/tools/sap_concur/list_exceptions.ts create mode 100644 apps/sim/tools/sap_concur/list_expected_expenses.ts create mode 100644 apps/sim/tools/sap_concur/list_expense_reports.ts create mode 100644 apps/sim/tools/sap_concur/list_expenses.ts create mode 100644 apps/sim/tools/sap_concur/list_itineraries.ts create mode 100644 apps/sim/tools/sap_concur/list_list_items.ts create mode 100644 apps/sim/tools/sap_concur/list_lists.ts create mode 100644 apps/sim/tools/sap_concur/list_receipts.ts create mode 100644 apps/sim/tools/sap_concur/list_report_comments.ts create mode 100644 apps/sim/tools/sap_concur/list_reports_to_approve.ts create mode 100644 apps/sim/tools/sap_concur/list_travel_profiles_summary.ts create mode 100644 apps/sim/tools/sap_concur/list_travel_request_comments.ts create mode 100644 apps/sim/tools/sap_concur/list_travel_requests.ts create mode 100644 apps/sim/tools/sap_concur/list_users.ts create mode 100644 apps/sim/tools/sap_concur/move_travel_request.ts create mode 100644 apps/sim/tools/sap_concur/recall_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/remove_all_attendees.ts create mode 100644 apps/sim/tools/sap_concur/search_locations.ts create mode 100644 apps/sim/tools/sap_concur/search_users.ts create mode 100644 apps/sim/tools/sap_concur/send_back_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/submit_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/types.ts create mode 100644 apps/sim/tools/sap_concur/update_allocation.ts create mode 100644 apps/sim/tools/sap_concur/update_expected_expense.ts create mode 100644 apps/sim/tools/sap_concur/update_expense.ts create mode 100644 apps/sim/tools/sap_concur/update_expense_report.ts create mode 100644 apps/sim/tools/sap_concur/update_list_item.ts create mode 100644 apps/sim/tools/sap_concur/update_travel_request.ts create mode 100644 apps/sim/tools/sap_concur/update_user.ts create mode 100644 apps/sim/tools/sap_concur/upload_exchange_rates.ts create mode 100644 apps/sim/tools/sap_concur/upload_receipt_image.ts create mode 100644 apps/sim/tools/sap_concur/utils.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index c4bc260742b..4092f8c10a7 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4141,6 +4141,25 @@ export function SapS4HanaIcon(props: SVGProps) { ) } +export function SapConcurIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function ServiceNowIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 48748d79ba4..b515c6ccdd2 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -155,6 +155,7 @@ import { RootlyIcon, S3Icon, SalesforceIcon, + SapConcurIcon, SapS4HanaIcon, SESIcon, SearchIcon, @@ -372,6 +373,7 @@ export const blockTypeToIconMap: Record = { rootly: RootlyIcon, s3: S3Icon, salesforce: SalesforceIcon, + sap_concur: SapConcurIcon, sap_s4hana: SapS4HanaIcon, search: SearchIcon, secrets_manager: SecretsManagerIcon, diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 6beab98ac26..af967f765a0 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -151,6 +151,7 @@ "rootly", "s3", "salesforce", + "sap_concur", "sap_s4hana", "search", "secrets_manager", diff --git a/apps/docs/content/docs/en/tools/sap_concur.mdx b/apps/docs/content/docs/en/tools/sap_concur.mdx new file mode 100644 index 00000000000..19ce53cb7f5 --- /dev/null +++ b/apps/docs/content/docs/en/tools/sap_concur.mdx @@ -0,0 +1,2761 @@ +--- +title: SAP Concur +description: Manage expense reports, travel requests, cash advances, and more in SAP Concur +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[SAP Concur](https://www.concur.com/) is a leading cloud-based platform for travel, expense, and invoice management. It helps organizations capture spend across every channel — corporate cards, receipts, mileage, and travel bookings — and route it through approval, audit, and reimbursement workflows. + +With SAP Concur, you can: + +- **Manage expense reports end-to-end**: Create reports, add expenses and itemizations, allocate costs, attach receipts, and run them through submit/approve/recall/send-back workflows +- **Capture spend at the source**: Upload receipt images, create quick expenses with images, and pull receipts back for matching and audit +- **Automate travel programs**: List and inspect trips and itineraries, manage travel requests with cash advance support, and read traveler profiles +- **Govern users and reference data**: Provision identities via SCIM v4.1, maintain custom lists for cost centers and projects, and look up locations, exchange rates, and budgets +- **Issue and track cash advances**: Create, retrieve, and issue cash advances tied to travel requests or expense reports + +In Sim, the SAP Concur integration empowers your agents to automate AP and travel workflows across every Concur datacenter. Use tool actions to: + +- **Drive expense automation**: Programmatically build, submit, and route expense reports without users opening the Concur UI +- **Sync identities and reference data**: Keep employees, cost centers, and project codes aligned with your HRIS and ERP systems +- **Process receipts at scale**: Forward receipt images from email or Slack into Concur for OCR, matching, and reimbursement +- **Build approval bots**: Surface pending reports to managers, summarize line items, and approve or send back with a single message + +These capabilities let you eliminate manual data entry, accelerate close cycles, and run your travel and expense program as code — all as part of your workflows. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Connect SAP Concur via OAuth 2.0. Manage expense reports and line items, allocations, attendees, comments, exceptions, quick expenses, receipts, travel requests and expected expenses, cash advances, itineraries, user identities, custom lists, budgets, exchange rates, and purchase requests across every Concur datacenter. + + + +## Tools + +### `sap_concur_approve_expense_report` + +Approve an expense report as a manager (PATCH /expensereports/v4/reports/\{reportId\}/approve). Required body field: comment. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `reportId` | string | Yes | Expense report ID to approve | +| `body` | json | Yes | Request body — `comment` is required by Concur \(e.g., \{ "comment": "Approved" \}\). If the report contains rejected expenses, `expenseRejectedComment` is also required. Optional fields: `expectedStepCode`, `expectedStepSequence`, `statusId` \(defaults to "A_APPR"\). | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty \(204 No Content\) | + +### `sap_concur_associate_attendees` + +Associate attendees with an expense (POST /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses/\{expenseId\}/attendees). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID | +| `body` | json | Yes | Attendee associations payload \(e.g., \{ "attendeeAssociations": \[...\] \}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Concur association response \(201 Created with URI\) | +| ↳ `uri` | string | Resource URI of the attendee associations collection | + +### `sap_concur_create_cash_advance` + +Create a cash advance (POST /cashadvance/v4.1/cashadvances). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `body` | json | Yes | Cash advance payload | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created cash advance payload | +| ↳ `cashAdvanceId` | string | Unique identifier of the created cash advance | + +### `sap_concur_create_expected_expense` + +Create an expected expense on a travel request (POST /travelrequest/v4/requests/\{requestUuid\}/expenses). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID | +| `userId` | string | No | User UUID acting on the request \(required when using a Company JWT, optional otherwise\) | +| `body` | json | Yes | Expected expense payload | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created expected expense payload | +| ↳ `id` | string | Expected expense identifier | +| ↳ `href` | string | Self-link to the resource | +| ↳ `expenseType` | json | Expense type \{id, name\} | +| ↳ `transactionDate` | string | Transaction date | +| ↳ `transactionAmount` | json | Transaction amount \{value, currencyCode\} | +| ↳ `postedAmount` | json | Posted amount \{value, currencyCode\} | +| ↳ `approvedAmount` | json | Approved amount \{value, currencyCode\} | +| ↳ `remainingAmount` | json | Remaining amount on the expected expense | +| ↳ `businessPurpose` | string | Business purpose of the expense | +| ↳ `location` | json | Location \{id, name, city, countryCode, countrySubDivisionCode, iataCode, locationType\} | +| ↳ `exchangeRate` | json | Exchange rate \{value, operation\} | +| ↳ `allocations` | json | Budget allocations array \(allocationId, allocationAmount, approvedAmount, postedAmount, expenseId, percentEdited, systemAllocation, percentage\) | +| ↳ `tripData` | json | Trip data \{agencyBooked, selfBooked, tripType \(ONE_WAY\|ROUND_TRIP\), legs\[\{id, returnLeg, startDate, startTime, startLocationDetail, startLocation, endLocation, class \{code,value\}, travelExceptionReasonCodes\}\], segmentType \{category, code\}\} | +| ↳ `parentRequest` | json | Parent travel request resource link \{href, id\} | +| ↳ `comments` | json | Comments sub-resource link \{href, id\} | + +### `sap_concur_create_expense_report` + +Create an expense report (POST /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports — supported contexts: TRAVELER, PROXY). Required body fields: name, policyId. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who will own the report | +| `contextType` | string | Yes | Access context: TRAVELER \(creating own report\) or PROXY \(creating on behalf of another user\) | +| `body` | json | Yes | Report payload — `name` and `policyId` are required. Optional fields: businessPurpose, comment, customData, countryCode, countrySubDivisionCode, etc. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created expense report \(Concur returns 201 with a URI to the new report\) | +| ↳ `uri` | string | URI of the newly created expense report | + +### `sap_concur_create_list_item` + +Create a list item (POST /list/v4/items). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `body` | json | Yes | List item payload. Required: listId, shortCode, value. Optional: parentId or parentCode \(mutually exclusive\). Note: Concur rejects shortCode/value containing hyphens. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created list item | +| ↳ `id` | string | List item UUID | +| ↳ `code` | string | Long code format for the item | +| ↳ `shortCode` | string | Short code identifier | +| ↳ `value` | string | Display value of the item | +| ↳ `parentId` | string | Parent item UUID \(omitted for first-level items\) | +| ↳ `level` | number | Hierarchy level \(1 for root items\) | +| ↳ `isDeleted` | boolean | Deletion status across all containing lists | +| ↳ `lists` | array | Lists containing this item | +| ↳ `id` | string | List UUID | +| ↳ `hasChildren` | boolean | Whether this item has children in the list | + +### `sap_concur_create_purchase_request` + +Create a purchase request (POST /purchaserequest/v4/purchaserequests). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `body` | json | Yes | Purchase request payload | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created purchase request payload | +| ↳ `id` | string | Identifier of the created purchase request | +| ↳ `uri` | string | Resource URI for the created purchase request | +| ↳ `errors` | array | Validation or processing errors returned by Concur | +| ↳ `errorCode` | string | Error code | +| ↳ `errorMessage` | string | Error message | +| ↳ `dataPath` | string | Path to the request data which has the error | + +### `sap_concur_create_quick_expense` + +Create a quick expense (POST /quickexpense/v4/users/\{userId\}/context/TRAVELER/quickexpenses). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who owns the quick expense | +| `contextType` | string | Yes | Access context: must be TRAVELER | +| `body` | json | Yes | Quick expense payload \(expenseTypeId, transactionAmount, transactionDate, etc.\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created quick expense response \(HTTP 201 Created\) | +| ↳ `quickExpenseIdUri` | string | URI of the created quick expense resource | + +### `sap_concur_create_quick_expense_with_image` + +Create a quick expense with an attached image (POST /quickexpense/v4/users/\{userId\}/context/\{contextType\}/quickexpenses/image). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: must be TRAVELER | +| `receipt` | json | Yes | Receipt image \(UserFile\). Allowed: PDF, PNG, JPEG, TIFF \(max 50MB\) | +| `body` | json | Yes | Quick expense payload \(transactionAmount, transactionDate, expenseTypeId, vendor, ...\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created quick expense response \(HTTP 201 with attached receipt image\) | +| ↳ `quickExpenseIdUri` | string | URI of the created quick expense resource | + +### `sap_concur_create_report_comment` + +Create a comment on a report (POST /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/comments). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `comment` | string | Yes | Comment text to add | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created comment response \(Concur returns 201 Created with URI\) | +| ↳ `uri` | string | Resource URI of the created comment | + +### `sap_concur_create_travel_request` + +Create a travel request (POST /travelrequest/v4/requests). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | No | Optional Concur user UUID — required when impersonating another user | +| `body` | json | Yes | Travel request payload \(name, purpose, startDate, endDate, requestPolicyId, etc.\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created travel request payload | +| ↳ `id` | string | Travel request UUID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `requestId` | string | Public-facing request ID \(4-6 alphanumeric characters\) | +| ↳ `name` | string | Request name | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `comment` | string | Last attached comment | +| ↳ `creationDate` | string | Creation timestamp | +| ↳ `lastModified` | string | Last modification timestamp | +| ↳ `submitDate` | string | Last submission timestamp | +| ↳ `startDate` | string | Trip start date \(ISO 8601\) | +| ↳ `endDate` | string | Trip end date \(ISO 8601\) | +| ↳ `startTime` | string | Trip start time \(HH:mm\) | +| ↳ `endTime` | string | Trip end time \(HH:mm\) | +| ↳ `approved` | boolean | Whether the request is approved | +| ↳ `pendingApproval` | boolean | Pending approval flag | +| ↳ `closed` | boolean | Closed flag | +| ↳ `everSentBack` | boolean | Ever-sent-back flag | +| ↳ `canceledPostApproval` | boolean | Canceled after approval flag | +| ↳ `approvalStatus` | json | Approval status | +| ↳ `code` | string | Status code \(NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK\) | +| ↳ `name` | string | Localized status name | +| ↳ `owner` | json | Travel request owner | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Owner first name | +| ↳ `lastName` | string | Owner last name | +| ↳ `approver` | json | Approver assigned to the request | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Approver first name | +| ↳ `lastName` | string | Approver last name | +| ↳ `policy` | json | Resource link to the applicable policy | +| ↳ `id` | string | Policy ID | +| ↳ `href` | string | Policy hyperlink | +| ↳ `type` | json | Request type | +| ↳ `code` | string | Request type code | +| ↳ `label` | string | Request type label | +| ↳ `mainDestination` | json | Main destination of the trip | +| ↳ `city` | string | City | +| ↳ `countryCode` | string | ISO country code | +| ↳ `countrySubDivisionCode` | string | ISO country sub-division code | +| ↳ `name` | string | Destination name | +| ↳ `totalApprovedAmount` | json | Total approved amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalPostedAmount` | json | Total posted amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalRemainingAmount` | json | Total remaining amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `operations` | array | Available workflow actions | +| ↳ `rel` | string | Operation name | +| ↳ `href` | string | Operation URL | +| ↳ `expenses` | array | Expected expenses attached to the request | +| ↳ `highestExceptionLevel` | string | Highest exception level \(NONE, WARNING, ERROR\) | +| ↳ `travelAgency` | json | Travel agency reference | +| ↳ `id` | string | Agency identifier | +| ↳ `href` | string | Agency URL | +| ↳ `template` | string | Template URL | +| ↳ `custom1` | json | Custom field 1 | +| ↳ `custom2` | json | Custom field 2 | +| ↳ `custom3` | json | Custom field 3 | +| ↳ `custom4` | json | Custom field 4 | + +### `sap_concur_create_user` + +Create a new user identity (POST /profile/identity/v4.1/Users). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `body` | json | Yes | SCIM User payload \(schemas, userName, name, emails, active, etc.\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Created SCIM User payload | + +### `sap_concur_delete_expected_expense` + +Delete an expected expense (DELETE /travelrequest/v4/expenses/\{expenseUuid\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `expenseUuid` | string | Yes | Expected expense UUID to delete | +| `userId` | string | No | User UUID acting on the request \(required when using a Company JWT, optional otherwise\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Returns boolean true on 200 OK when the expected expense is deleted. | + +### `sap_concur_delete_expense` + +Delete an expense (DELETE /expensereports/v4/reports/\{reportId\}/expenses/\{expenseId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty body on success \(HTTP 204 No Content\). Error details when status is non-2xx | + +### `sap_concur_delete_expense_report` + +Delete an expense report (DELETE /expensereports/v4/reports/\{reportId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `reportId` | string | Yes | Expense report ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty \(204 No Content\) | + +### `sap_concur_delete_list_item` + +Delete a list item (DELETE /list/v4/items/\{itemId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `itemId` | string | Yes | List item UUID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty body on success \(HTTP 204 No Content\). Error details when status is non-2xx | + +### `sap_concur_delete_travel_request` + +Delete a travel request (DELETE /travelrequest/v4/requests/\{requestUuid\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID to delete | +| `userId` | string | No | Optional Concur user UUID — required when impersonating another user | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Concur delete response payload \(boolean true on 200 OK\) | + +### `sap_concur_delete_user` + +Delete a user identity (DELETE /profile/identity/v4.1/Users/\{id\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userUuid` | string | Yes | User UUID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Deletion response — empty body on HTTP 204 No Content | + +### `sap_concur_get_allocation` + +Get a single allocation (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/allocations/\{allocationId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `allocationId` | string | Yes | Allocation ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Allocation detail payload | +| ↳ `allocationId` | string | Unique allocation identifier | +| ↳ `accountCode` | string | Ledger account code | +| ↳ `overLimitAccountCode` | string | Account code applied to amounts over the per-allocation limit | +| ↳ `percentage` | number | Allocation percentage | +| ↳ `allocationAmount` | json | Allocation amount \(value, currencyCode\) | +| ↳ `value` | number | Amount value | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `approvedAmount` | json | Pro-rated approved amount \(value, currencyCode\) | +| ↳ `value` | number | Amount value | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `claimedAmount` | json | Requested reimbursement amount \(value, currencyCode\) | +| ↳ `value` | number | Amount value | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `customData` | array | Custom field values \(id, value, isValid\) | +| ↳ `expenseId` | string | Associated expense identifier | +| ↳ `isSystemAllocation` | boolean | True when system-managed | +| ↳ `isPercentEdited` | boolean | True when the percentage was manually edited | + +### `sap_concur_get_budget` + +Get a budget item header by ID (GET /budget/v4/budgetItemHeader/\{id\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `budgetId` | string | Yes | Budget item header ID \(syncguid\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Budget header detail payload | +| ↳ `id` | string | Budget item header ID | +| ↳ `name` | string | Admin-facing budget name | +| ↳ `description` | string | User-friendly display name | +| ↳ `budgetItemStatusType` | string | Status: OPEN, CLOSED, or REMOVED | +| ↳ `budgetType` | string | Type: PERSONAL_USE, BUDGET, RESTRICTED, or TEAM | +| ↳ `periodType` | string | Period type: YEARLY, QUARTERLY, MONTHLY, or DATE_RANGE | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `isTest` | boolean | Test budget flag | +| ↳ `active` | boolean | Display availability flag | +| ↳ `owned` | boolean | Caller ownership flag | +| ↳ `annualBudget` | number | Total annual budget amount | +| ↳ `createdDate` | string | UTC creation timestamp | +| ↳ `lastModifiedDate` | string | UTC modification timestamp | +| ↳ `fiscalYear` | json | Fiscal year reference \(id, name, startDate, endDate, status\) | +| ↳ `budgetAmounts` | json | Aggregate spend amounts \(pendingAmount, spendAmount, unExpensedAmount, availableAmount, adjustedBudgetAmount, consumedPercent, threshold\) | +| ↳ `owner` | json | Owner user \(externalUserCUUID, employeeUuid, email, employeeId, name\) | +| ↳ `budgetManagers` | array | Manager user objects | +| ↳ `budgetApprovers` | array | Approver user objects | +| ↳ `budgetViewers` | array | Viewer user objects | +| ↳ `budgetTeamMembers` | array | Team member entries \(budgetPerson, startDate, endDate, active, status\) | +| ↳ `budgetCategory` | json | Linked category \(id, name, description, statusType\) | +| ↳ `costObjects` | array | Tracking field values \(fieldDefinitionId, code, value, operator\) | +| ↳ `budgetItemDetails` | array | Per-period detail entries \(id, currencyCode, amount, budgetItemDetailStatusType, fiscalPeriod, budgetAmounts\) | +| ↳ `dateRange` | json | Date range for DATE_RANGE budgets \(startDate, endDate\) | + +### `sap_concur_get_cash_advance` + +Get a cash advance (GET /cashadvance/v4.1/cashadvances/\{cashAdvanceId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `cashAdvanceId` | string | Yes | Cash advance ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Cash advance detail payload | +| ↳ `cashAdvanceId` | string | Unique identifier of the cash advance | +| ↳ `name` | string | Cash advance name | +| ↳ `purpose` | string | Purpose for the cash advance | +| ↳ `comment` | string | Comment recorded on the cash advance | +| ↳ `accountCode` | string | Account code linked to the employee | +| ↳ `requestDate` | string | Datetime the cash advance was requested \(UTC, YYYY-MM-DD hh:mm:ss\) | +| ↳ `issuedDate` | string | Datetime the cash advance was issued \(UTC, YYYY-MM-DD hh:mm:ss\) | +| ↳ `lastModifiedDate` | string | Datetime the cash advance was last modified \(UTC, YYYY-MM-DD hh:mm:ss\) | +| ↳ `hasReceipts` | boolean | Whether the cash advance has receipts | +| ↳ `reimbursementCurrency` | string | Reimbursement currency \(3-letter ISO 4217 currency code\) | +| ↳ `amountRequested` | json | Amount requested for the cash advance | +| ↳ `amount` | string | Requested amount value | +| ↳ `currency` | string | 3-letter ISO 4217 currency code | +| ↳ `availableBalance` | json | Unsubmitted balance for the cash advance | +| ↳ `amount` | string | Balance amount | +| ↳ `currency` | string | 3-letter ISO 4217 currency code | +| ↳ `exchangeRate` | json | Exchange rate that applies to the cash advance | +| ↳ `value` | string | Exchange rate value | +| ↳ `operation` | string | Exchange rate operation \(MULTIPLY\) | +| ↳ `approvalStatus` | json | Approval status of the cash advance | +| ↳ `code` | string | Status code | +| ↳ `name` | string | Status display name | +| ↳ `paymentType` | json | Payment type for the cash advance | +| ↳ `paymentCode` | string | Payment type code | +| ↳ `description` | string | Payment method description | + +### `sap_concur_upload_exchange_rates` + +Bulk upload up to 100 custom exchange rates (POST /exchangerate/v4/rates). Body contains a currency_sets array, each with from_crn_code, to_crn_code, start_date (YYYY-MM-DD), and rate. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `body` | json | Yes | Bulk upload body: \{ currency_sets: \[\{ from_crn_code, to_crn_code, start_date: "YYYY-MM-DD", rate \}\] \} \(max 100 entries\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Bulk-upload exchange rate response \(Exchange Rate v4\) | +| ↳ `overallStatus` | string | Overall result status for the bulk upload \(e.g. SUCCESS, FAILURE\) | +| ↳ `message` | string | Top-level result message | +| ↳ `currencySets` | json | Per-row results: array of \{ from_crn_code, to_crn_code, start_date, rate, statusCode, statusMessage \} | + +### `sap_concur_get_expected_expense` + +Get an expected expense (GET /travelrequest/v4/expenses/\{expenseUuid\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `expenseUuid` | string | Yes | Expected expense UUID | +| `userId` | string | No | User UUID acting on the request \(optional\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Expected expense payload | +| ↳ `id` | string | Expected expense identifier | +| ↳ `href` | string | Self-link | +| ↳ `expenseType` | json | Expense type \{id, name\} | +| ↳ `transactionDate` | string | Transaction date | +| ↳ `transactionAmount` | json | Transaction amount \{value, currencyCode\} | +| ↳ `postedAmount` | json | Posted amount \{value, currencyCode\} | +| ↳ `approvedAmount` | json | Approved amount \{value, currencyCode\} | +| ↳ `remainingAmount` | json | Remaining amount on the expected expense | +| ↳ `businessPurpose` | string | Business purpose of the expense | +| ↳ `location` | json | Location \{id, name, city, countryCode, countrySubDivisionCode, iataCode, locationType\} | +| ↳ `exchangeRate` | json | Exchange rate \{value, operation\} | +| ↳ `allocations` | json | Budget allocations array | +| ↳ `tripData` | json | Trip data \{agencyBooked, selfBooked, tripType \(ONE_WAY\|ROUND_TRIP\), legs\[\{id, returnLeg, startDate, startTime, startLocationDetail, startLocation, endLocation, class \{code,value\}, travelExceptionReasonCodes\}\], segmentType \{category, code\}\} | +| ↳ `parentRequest` | json | Parent travel request resource link \{href, id\} | +| ↳ `comments` | json | Comments sub-resource link \{href, id\} | + +### `sap_concur_get_expense` + +Get a single expense (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses/\{expenseId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER, MANAGER, or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Expense detail \(ReportExpenseDetail\) payload | +| ↳ `expenseId` | string | Expense identifier | +| ↳ `allocationSetId` | string | Identifier of the associated allocation set | +| ↳ `allocationState` | string | FULLY_ALLOCATED, NOT_ALLOCATED, or PARTIALLY_ALLOCATED | +| ↳ `expenseType` | json | Expense type \{id, name, code, isDeleted\} | +| ↳ `paymentType` | json | Payment type \{id, name, code\} | +| ↳ `expenseSource` | string | Source of the expense \(CASH, CCARD, EBOOKING, etc.\) | +| ↳ `transactionDate` | string | Transaction date \(YYYY-MM-DD\) | +| ↳ `budgetAccrualDate` | string | Budget accrual date | +| ↳ `transactionAmount` | json | Transaction amount \{currencyCode, value\} | +| ↳ `postedAmount` | json | Posted amount in report currency \{currencyCode, value\} | +| ↳ `claimedAmount` | json | Non-personal claimed amount \{currencyCode, value\} | +| ↳ `approvedAmount` | json | Approved amount \{currencyCode, value\} | +| ↳ `approverAdjustedAmount` | json | Total amount adjusted by the approver | +| ↳ `exchangeRate` | json | Exchange rate \{value, operation\} | +| ↳ `vendor` | json | Vendor info \{id, name, description\} | +| ↳ `location` | json | Location \{id, name, city, countryCode, countrySubDivisionCode\} | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `comment` | string | Free-form comment associated with the expense | +| ↳ `isExpenseBillable` | boolean | Billable flag | +| ↳ `isPersonalExpense` | boolean | Personal-expense flag | +| ↳ `isExpenseRejected` | boolean | Whether the expense was rejected | +| ↳ `isExcludedFromCashAdvanceByUser` | boolean | Whether the user excluded this from cash advance | +| ↳ `isImageRequired` | boolean | Whether a receipt image is required | +| ↳ `isPaperReceiptRequired` | boolean | Whether a paper receipt is required | +| ↳ `isPaperReceiptReceived` | boolean | Whether a paper receipt was received | +| ↳ `isAutoCreated` | boolean | Auto-creation indicator | +| ↳ `hasBlockingExceptions` | boolean | Whether submission-blocking exceptions exist | +| ↳ `hasExceptions` | boolean | Whether any exceptions exist | +| ↳ `hasMissingReceiptDeclaration` | boolean | Affidavit declaration status | +| ↳ `attendeeCount` | number | Number of attendees | +| ↳ `receiptImageId` | string | Identifier of the attached receipt image | +| ↳ `ereceiptImageId` | string | eReceipt image identifier | +| ↳ `receiptType` | json | Receipt \{id, status\} | +| ↳ `imageCertificationStatus` | string | Receipt image processing/certification status | +| ↳ `ticketNumber` | string | Associated travel ticket number | +| ↳ `travel` | json | Travel data \(airline, car rental, hotel, etc.\) | +| ↳ `travelAllowance` | json | Travel allowance association data | +| ↳ `mileage` | json | Mileage details \(odometerStart, odometerEnd, totalDistance, ...\) | +| ↳ `expenseTaxSummary` | json | Aggregated tax data for the expense | +| ↳ `taxRateLocation` | string | Tax rate location: FOREIGN, HOME, or OUT_OF_PROVINCE | +| ↳ `fuelTypeListItem` | json | Fuel type list item \{id, value, isValid\} | +| ↳ `merchantTaxId` | string | Merchant tax identifier | +| ↳ `customData` | json | Array of custom field values \[\{id, value, isValid\}\] | +| ↳ `parentExpenseId` | string | Identifier of the parent expense \(for itemizations\) | +| ↳ `authorizationRequestExpenseId` | string | Linked travel-request expected expense identifier | +| ↳ `jptRouteId` | string | Japan Public Transport route id | +| ↳ `invoiceId` | string | Invoice identifier | +| ↳ `governmentInvoiceId` | string | Government invoice identifier | +| ↳ `lastModifiedDate` | string | Last modified timestamp | +| ↳ `expenseSourceIdentifiers` | json | Source reference identifiers | +| ↳ `links` | json | HATEOAS links for the expense | + +### `sap_concur_get_expense_report` + +Retrieve a single expense report header by id via Expense Report v4 (/expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who owns the report | +| `contextType` | string | Yes | Access context: TRAVELER \(own report\), MANAGER \(report under approval\), PROCESSOR, or PROXY | +| `reportId` | string | Yes | Expense report ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Concur expense report header \(ReportDetails\) | +| ↳ `reportId` | string | Unique report identifier | +| ↳ `reportNumber` | string | Report number | +| ↳ `reportFormId` | string | Report form ID | +| ↳ `policyId` | string | Policy ID applied to the report | +| ↳ `policy` | string | Policy name | +| ↳ `name` | string | Report name | +| ↳ `currencyCode` | string | ISO currency code | +| ↳ `currency` | string | Currency name | +| ↳ `approvalStatus` | string | Approval status name | +| ↳ `approvalStatusId` | string | Approval status identifier | +| ↳ `paymentStatus` | string | Payment status name | +| ↳ `paymentStatusId` | string | Payment status identifier | +| ↳ `ledger` | string | Ledger name | +| ↳ `ledgerId` | string | Ledger identifier | +| ↳ `userId` | string | Owner user UUID | +| ↳ `reportDate` | string | Report date \(YYYY-MM-DD\) | +| ↳ `creationDate` | string | Creation timestamp \(ISO 8601\) | +| ↳ `submitDate` | string | Submit timestamp \(ISO 8601\) or null | +| ↳ `startDate` | string | Report period start \(YYYY-MM-DD\) | +| ↳ `endDate` | string | Report period end \(YYYY-MM-DD\) | +| ↳ `approvedAmount` | json | Amount approved \{ value, currencyCode \} | +| ↳ `claimedAmount` | json | Amount claimed \{ value, currencyCode \} | +| ↳ `reportTotal` | json | Report total \{ value, currencyCode \} | +| ↳ `amountDueEmployee` | json | Amount due employee | +| ↳ `amountDueCompany` | json | Amount due company | +| ↳ `amountDueCompanyCard` | json | Amount due company card | +| ↳ `amountCompanyPaid` | json | Amount company has paid | +| ↳ `personalAmount` | json | Personal portion of the report | +| ↳ `paymentConfirmedAmount` | json | Confirmed payment amount | +| ↳ `amountNotApproved` | json | Amount not approved | +| ↳ `totalAmountPaidEmployee` | json | Total amount paid to employee | +| ↳ `concurAuditStatus` | string | Concur audit status | +| ↳ `isFinancialIntegrationEnabled` | boolean | Whether financial integration is enabled | +| ↳ `isSubmitted` | boolean | Whether the report has been submitted | +| ↳ `isSentBack` | boolean | Whether the report has been sent back | +| ↳ `isReopened` | boolean | Whether the report was reopened | +| ↳ `isReportEverSentBack` | boolean | Whether the report was ever sent back | +| ↳ `canRecall` | boolean | Whether the report can be recalled | +| ↳ `canAddExpense` | boolean | Whether expenses can be added to the report | +| ↳ `canReopen` | boolean | Whether the report can be reopened | +| ↳ `isReceiptImageRequired` | boolean | Whether receipt images are required | +| ↳ `isReceiptImageAvailable` | boolean | Whether receipt images are available | +| ↳ `isPaperReceiptsReceived` | boolean | Whether paper receipts were received | +| ↳ `isPendingDelegatorReview` | boolean | Whether pending delegator review | +| ↳ `isFundsAndGrantsIntegrationEligible` | boolean | Funds and grants eligibility | +| ↳ `hasReceivedCashAdvanceReturns` | boolean | Whether cash advance returns received | +| ↳ `analyticsGroupId` | string | Analytics group ID | +| ↳ `hierarchyNodeId` | string | Hierarchy node ID | +| ↳ `allocationFormId` | string | Allocation form ID | +| ↳ `countryCode` | string | ISO country code | +| ↳ `countrySubDivisionCode` | string | ISO country subdivision code | +| ↳ `country` | string | Country name | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `comment` | string | Header-level comment on the report | +| ↳ `reportVersion` | number | Report version number | +| ↳ `reportType` | string | Report type identifier | +| ↳ `cardProgramStatementPeriodId` | string | Card program statement period ID | +| ↳ `defaultFieldAccess` | string | Default field access \(HD/RO/RW\) | +| ↳ `imageStatus` | string | Image status | +| ↳ `receiptContainerId` | string | Receipt container ID | +| ↳ `receiptStatus` | string | Receipt status | +| ↳ `sponsorId` | string | Sponsor ID | +| ↳ `submitterId` | string | Submitter user ID | +| ↳ `taxConfigId` | string | Tax configuration ID | +| ↳ `redirectFund` | json | Redirect fund object \{ amount, creditCardId \} | +| ↳ `customData` | array | Array of custom data \{ id, value, isValid, listItemUrl \} | +| ↳ `employee` | json | Employee object \{ employeeId, employeeUuid \} | +| ↳ `links` | array | HATEOAS links | + +### `sap_concur_get_itemizations` + +Get expense itemizations (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses/\{expenseId\}/itemizations). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER, MANAGER, or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of itemizations \(ReportExpenseSummary\[\]\) | +| ↳ `id` | string | Itemization identifier | +| ↳ `expenseId` | string | Itemization expense id | +| ↳ `allocations` | array | Allocations applied to the itemization | +| ↳ `expenseType` | json | Expense type \{id, name, code, isDeleted\} | +| ↳ `transactionDate` | string | Transaction date \(YYYY-MM-DD\) | +| ↳ `transactionAmount` | json | Transaction amount | +| ↳ `postedAmount` | json | Posted amount | +| ↳ `approvedAmount` | json | Approved amount | +| ↳ `claimedAmount` | json | Claimed amount | +| ↳ `approverAdjustedAmount` | json | Approver-adjusted amount | +| ↳ `paymentType` | json | Payment type | +| ↳ `vendor` | json | Vendor info | +| ↳ `location` | json | Location info | +| ↳ `allocationState` | string | Allocation state | +| ↳ `allocationSetId` | string | Allocation set identifier | +| ↳ `attendeeCount` | number | Attendee count | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `hasBlockingExceptions` | boolean | Has blocking exceptions | +| ↳ `hasExceptions` | boolean | Has exceptions | +| ↳ `isPersonalExpense` | boolean | Personal expense | +| ↳ `links` | array | HATEOAS links | + +### `sap_concur_get_itinerary` + +Get a single trip/itinerary (GET /api/travel/trip/v1.1/\{tripID\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `tripId` | string | Yes | Trip ID | +| `useridType` | string | No | User identifier type \(login, xmlsyncid, uuid\) | +| `useridValue` | string | No | User identifier value \(paired with useridType\) | +| `systemFormat` | string | No | Optional system format \(e.g., GDS\) for the response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Trip detail payload \(Itinerary v1.1\) | +| ↳ `ItinLocator` | string | Concur trip locator \(trip ID\) | +| ↳ `ClientLocator` | string | Client \(booking source\) trip locator | +| ↳ `ItinSourceName` | string | Booking source name | +| ↳ `BookedVia` | string | How the trip was booked \(e.g. ConcurTravel, Direct\) | +| ↳ `TripName` | string | Trip name | +| ↳ `Status` | string | Trip status \(e.g. Confirmed, Cancelled\) | +| ↳ `Description` | string | Trip description | +| ↳ `Comments` | string | Comments attached to the trip | +| ↳ `CancelComments` | string | Cancellation comments \(when applicable\) | +| ↳ `ProjectName` | string | Associated project name | +| ↳ `StartDateUtc` | string | Trip start datetime in UTC | +| ↳ `EndDateUtc` | string | Trip end datetime in UTC | +| ↳ `StartDateLocal` | string | Trip start datetime in local time | +| ↳ `EndDateLocal` | string | Trip end datetime in local time | +| ↳ `DateCreatedUtc` | string | Trip creation timestamp \(UTC\) | +| ↳ `DateModifiedUtc` | string | Trip last-modified timestamp \(UTC\) | +| ↳ `DateBookedLocal` | string | Booking date in local time | +| ↳ `UserLoginId` | string | Login id of the trip owner | +| ↳ `BookedByFirstName` | string | First name of the booker | +| ↳ `BookedByLastName` | string | Last name of the booker | +| ↳ `IsPersonal` | boolean | Whether the trip is flagged personal | +| ↳ `RuleViolations` | array | Travel rule violations attached to the trip | +| ↳ `Bookings` | array | Bookings \(air/hotel/car/rail\) attached to the trip | + +### `sap_concur_get_list` + +Get a single custom list (GET /list/v4/lists/\{listId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `listId` | string | Yes | List ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | List detail payload | +| ↳ `id` | string | Unique identifier \(UUID\) of the list | +| ↳ `value` | string | Name of the list | +| ↳ `levelCount` | number | Number of levels in the list | +| ↳ `searchCriteria` | string | Search attribute \(TEXT or CODE\) | +| ↳ `displayFormat` | string | Display order \(\(CODE\) TEXT or TEXT \(CODE\)\) | +| ↳ `category` | json | List category | +| ↳ `id` | string | Category UUID | +| ↳ `type` | string | Category type | +| ↳ `isReadOnly` | boolean | Whether the list is read-only | +| ↳ `isDeleted` | boolean | Whether the list has been deleted | +| ↳ `managedBy` | string | Identifier of the managing application or service | +| ↳ `externalThreshold` | number | Threshold from where the level starts being external | + +### `sap_concur_get_list_item` + +Get a single list item (GET /list/v4/items/\{itemId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `itemId` | string | Yes | List item ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | List item detail payload | +| ↳ `id` | string | List item UUID | +| ↳ `code` | string | Long code format for the item | +| ↳ `shortCode` | string | Short code identifier | +| ↳ `value` | string | Display value of the item | +| ↳ `parentId` | string | Parent item UUID \(omitted for first-level items\) | +| ↳ `level` | number | Hierarchy level \(1 for root items\) | +| ↳ `isDeleted` | boolean | Deletion status across all containing lists | +| ↳ `lists` | array | Lists containing this item | +| ↳ `id` | string | List UUID | +| ↳ `hasChildren` | boolean | Whether this item has children in the list | + +### `sap_concur_get_purchase_request` + +Get a purchase request by ID (GET /purchaserequest/v4/purchaserequests/\{id\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `purchaseRequestId` | string | Yes | Purchase request ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Purchase request detail payload | +| ↳ `purchaseRequestId` | string | Unique identifier of the purchase request | +| ↳ `purchaseRequestNumber` | string | Human-readable purchase request number | +| ↳ `purchaseRequestQueueStatus` | string | Queue status of the purchase request | +| ↳ `purchaseRequestWorkflowStatus` | string | Workflow status of the purchase request | +| ↳ `purchaseOrders` | array | Purchase orders generated from the request | +| ↳ `purchaseOrderNumber` | string | Purchase order number | +| ↳ `purchaseRequestExceptions` | array | Exceptions raised on the purchase request | +| ↳ `eventCode` | string | Event code | +| ↳ `exceptionCode` | string | Exception code | +| ↳ `isCleared` | boolean | Whether the exception has been cleared | +| ↳ `prExceptionId` | string | Identifier of the exception record | +| ↳ `message` | string | Exception message | + +### `sap_concur_get_receipt` + +Get a single receipt by ID (GET /receipts/v4/\{receiptId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `receiptId` | string | Yes | Receipt ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Receipt detail payload | +| ↳ `id` | string | Receipt identifier | +| ↳ `userId` | string | Owning user UUID | +| ↳ `dateTimeReceived` | string | Timestamp when the receipt was received \(ISO 8601\) | +| ↳ `receipt` | json | Parsed receipt JSON object | +| ↳ `image` | string | Receipt image URL or data reference | +| ↳ `validationSchema` | string | Schema used to validate the receipt | +| ↳ `self` | string | URL to this receipt resource | +| ↳ `template` | string | URL template for receipts | + +### `sap_concur_get_receipt_status` + +Get receipt processing status (GET /receipts/v4/status/\{receiptId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `receiptId` | string | Yes | Receipt ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Receipt status payload | +| ↳ `status` | string | Processing status: ACCEPTED, PROCESSING, PROCESSED, or FAILED | +| ↳ `logs` | array | Array of log entries | +| ↳ `logLevel` | string | Log level | +| ↳ `message` | string | Log message | +| ↳ `timestamp` | string | Log timestamp | + +### `sap_concur_get_travel_profile` + +Get a travel profile (GET /api/travelprofile/v2.0/profile). Returns the calling user by default; pass userid_type and userid_value to impersonate. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `useridType` | string | No | Identifier type: login, xmlsyncid, or uuid | +| `useridValue` | string | No | Identifier value \(login id, xml sync id, or UUID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Travel profile payload. Concur returns XML; downstream may parse it to a best-effort JSON object with the documented top-level sections. | +| ↳ `General` | json | General profile info \(NamePrefix, FirstName, MiddleName, LastName, NameSuffix, JobTitle, CompanyEmployeeID, EmailAddress, RuleClass, TravelConfigID, etc.\) | +| ↳ `Telephones` | json | Telephone numbers \(Telephone\[\] with Type, CountryCode, PhoneNumber, etc.\) | +| ↳ `Addresses` | json | Address records \(Address\[\] with Type, Street, City, StateProvince, etc.\) | +| ↳ `DriversLicenses` | array | Drivers license records | +| ↳ `NationalIDs` | array | National ID records | +| ↳ `EmailAddresses` | json | Email addresses \(EmailAddress\[\] with Type, Address, Contact, Verified\) | +| ↳ `EmergencyContact` | json | Emergency contact \(Name, Relationship, Phones, Address\) | +| ↳ `Air` | json | Air travel preferences \(HomeAirport, Seat, Meal, AirOther, AirMemberships\) | +| ↳ `Rail` | json | Rail preferences \(Seat, Coach, Berth, Other, RailMemberships\) | +| ↳ `Hotel` | json | Hotel preferences \(SmokingCode, RoomType, HotelOther, HotelMemberships, Accessibility flags\) | +| ↳ `Car` | json | Car rental preferences \(CarSmokingCode, CarType, CarMemberships, etc.\) | +| ↳ `CustomFields` | json | Custom-defined fields configured by the company | +| ↳ `RatePreferences` | json | Rate preferences \(e.g. AAA, AARP, government, military rates\) | +| ↳ `DiscountCodes` | json | Discount codes available to the traveler | +| ↳ `HasNoPassport` | boolean | Whether the traveler has no passport on file | +| ↳ `Roles` | json | Role assignments \(TravelManager, Assistant, etc.\) | +| ↳ `Sponsors` | json | Sponsor information for guest travelers | +| ↳ `TSAInfo` | json | TSA SecureFlight info \(Gender, DateOfBirth, NoMiddleName, etc.\) | +| ↳ `Passports` | json | Passport documents \(Passport\[\] with PassportNumber, Country, Expiration\) | +| ↳ `Visas` | json | Visa documents \(Visa\[\] with VisaNationality, VisaNumber, etc.\) | +| ↳ `UnusedTickets` | json | Unused ticket records | +| ↳ `SouthwestUnusedTickets` | json | Southwest-specific unused ticket records | +| ↳ `AdvantageMemberships` | json | Advantage program memberships | +| ↳ `XmlSyncId` | string | XML sync identifier for the user | +| ↳ `LoginId` | string | Concur login id | +| ↳ `ProfileLastModifiedUTC` | string | UTC timestamp the profile was last modified | + +### `sap_concur_get_travel_request` + +Get a single travel request (GET /travelrequest/v4/requests/\{requestUuid\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID | +| `userId` | string | No | Optional Concur user UUID — required when impersonating another user | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Travel request detail payload | +| ↳ `id` | string | Travel request UUID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `requestId` | string | Public-facing request ID \(4-6 alphanumeric characters\) | +| ↳ `name` | string | Request name | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `comment` | string | Last attached comment | +| ↳ `creationDate` | string | Creation timestamp | +| ↳ `lastModified` | string | Last modification timestamp | +| ↳ `submitDate` | string | Last submission timestamp | +| ↳ `authorizedDate` | string | Date when approval was completed | +| ↳ `approvalLimitDate` | string | Required approval deadline | +| ↳ `startDate` | string | Trip start date \(ISO 8601\) | +| ↳ `endDate` | string | Trip end date \(ISO 8601\) | +| ↳ `startTime` | string | Trip start time \(HH:mm\) | +| ↳ `endTime` | string | Trip end time \(HH:mm\) | +| ↳ `pnr` | string | Passenger record number | +| ↳ `approved` | boolean | Whether the request is approved | +| ↳ `pendingApproval` | boolean | Pending approval flag | +| ↳ `closed` | boolean | Closed flag | +| ↳ `everSentBack` | boolean | Ever-sent-back flag | +| ↳ `canceledPostApproval` | boolean | Canceled after approval flag | +| ↳ `isParentRequest` | boolean | Parent request flag | +| ↳ `parentRequestId` | string | Parent budget request ID | +| ↳ `allocationFormId` | string | Allocation form identifier | +| ↳ `highestExceptionLevel` | string | Highest exception level \(WARNING, ERROR, NONE\) | +| ↳ `approvalStatus` | json | Approval status | +| ↳ `code` | string | Status code \(NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK\) | +| ↳ `name` | string | Localized status name | +| ↳ `owner` | json | Travel request owner | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Owner first name | +| ↳ `lastName` | string | Owner last name | +| ↳ `approver` | json | Approver assigned to the request | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Approver first name | +| ↳ `lastName` | string | Approver last name | +| ↳ `policy` | json | Resource link to the applicable policy | +| ↳ `id` | string | Policy ID | +| ↳ `href` | string | Policy hyperlink | +| ↳ `type` | json | Request type | +| ↳ `code` | string | Request type code | +| ↳ `label` | string | Request type label | +| ↳ `mainDestination` | json | Main destination of the trip | +| ↳ `city` | string | City | +| ↳ `countryCode` | string | ISO country code | +| ↳ `countrySubDivisionCode` | string | ISO country sub-division code | +| ↳ `name` | string | Destination name | +| ↳ `totalApprovedAmount` | json | Total approved amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalPostedAmount` | json | Total posted amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalRemainingAmount` | json | Total remaining amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `expenses` | array | Resource links to expected expenses | +| ↳ `cashAdvances` | json | Resource link to cash advances | +| ↳ `id` | string | Resource ID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `comments` | json | Resource link to comments | +| ↳ `id` | string | Resource ID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `exceptions` | json | Resource link to exceptions | +| ↳ `id` | string | Resource ID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `travelAgency` | json | Resource link to travel agency | +| ↳ `id` | string | Resource ID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `parentRequest` | json | Resource link to parent request | +| ↳ `id` | string | Resource ID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `eventRequest` | json | Resource link to parent event request | +| ↳ `id` | string | Resource ID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `operations` | array | Available workflow actions | +| ↳ `rel` | string | Operation name | +| ↳ `href` | string | Operation URL | +| ↳ `expensePolicy` | json | Expense policy reference | +| ↳ `id` | string | Policy identifier | +| ↳ `href` | string | Policy URL | +| ↳ `custom1` | json | Custom field 1 | +| ↳ `custom2` | json | Custom field 2 | +| ↳ `custom3` | json | Custom field 3 | +| ↳ `custom4` | json | Custom field 4 | + +### `sap_concur_get_user` + +Get a single user by UUID (GET /profile/identity/v4.1/Users/\{id\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userUuid` | string | Yes | User UUID | +| `attributes` | string | No | Comma-separated SCIM attributes to include in the response | +| `excludedAttributes` | string | No | Comma-separated SCIM attributes to exclude from the response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | SCIM User identity payload | + +### `sap_concur_issue_cash_advance` + +Issue a cash advance (POST /cashadvance/v4.1/cashadvances/\{cashAdvanceId\}/issue). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `cashAdvanceId` | string | Yes | Cash advance ID to issue | +| `body` | json | No | Optional request body | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Issue cash advance result payload | +| ↳ `issuedDate` | string | Date the cash advance was issued \(YYYY-MM-DD\) | +| ↳ `status` | json | Cash advance status after the issue action | +| ↳ `code` | string | Status code | +| ↳ `name` | string | Status display name | + +### `sap_concur_list_allocations` + +List allocations on an expense (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses/\{expenseId\}/allocations). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Allocations list payload | + +### `sap_concur_list_attendee_associations` + +List attendees associated with an expense (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses/\{expenseId\}/attendees). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Attendees list payload | +| ↳ `noShowAttendeeCount` | number | Number of unnamed/no-show attendees | +| ↳ `expenseAttendeeList` | array | Attendees associated with the expense, including amounts | +| ↳ `attendeeId` | string | Unique identifier of the attendee | +| ↳ `transactionAmount` | json | Expense portion assigned to this attendee | +| ↳ `value` | number | Numeric amount | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `approvedAmount` | json | Approved amount in report currency | +| ↳ `value` | number | Numeric amount | +| ↳ `currencyCode` | string | ISO 4217 currency code | +| ↳ `isAmountUserEdited` | boolean | Whether the amount was manually edited | +| ↳ `isTraveling` | boolean | Whether the attendee is traveling \(affects tax calculations\) | +| ↳ `associatedAttendeeCount` | number | Total attendee count; greater than 1 indicates unnamed attendees | +| ↳ `versionNumber` | number | Version number preserving previous attendee state | +| ↳ `customData` | array | Custom field values for the association | +| ↳ `id` | string | Custom field identifier | +| ↳ `value` | string | Custom field value \(max 48 characters\) | +| ↳ `isValid` | boolean | Whether the value passes validation | +| ↳ `listItemUrl` | string | HATEOAS link for list items | + +### `sap_concur_list_budget_categories` + +List budget categories (GET /budget/v4/budgetCategory). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Budget categories collection payload | + +### `sap_concur_list_budgets` + +List budget item headers (GET /budget/v4/budgetItemHeader). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `adminView` | boolean | No | When true, returns all budgets the caller can administer \(default false\) | +| `offset` | number | No | Page offset \(Concur returns up to 50 budget headers per page\) | +| `responseSchema` | string | No | Response schema variant: "COMPACT" returns a smaller payload | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Budget headers collection payload | +| ↳ `offset` | number | Page offset | +| ↳ `limit` | number | Page size | +| ↳ `totalCount` | number | Total result count | + +### `sap_concur_list_exceptions` + +List exceptions on a report (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/exceptions). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER, MANAGER, or PROXY | +| `reportId` | string | Yes | Expense report ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of report header exception entries | +| ↳ `exceptionCode` | string | Unique exception code | +| ↳ `exceptionVisibility` | string | Visibility scope: ALL, APPROVER_PROCESSOR, or PROCESSOR | +| ↳ `isBlocking` | boolean | Whether the exception prevents report submission | +| ↳ `message` | string | Human-readable description of the exception | +| ↳ `expenseId` | string | Related expense entry ID | +| ↳ `allocationId` | string | Related allocation ID, if any | +| ↳ `parentExpenseId` | string | Parent expense ID for itemized entries | + +### `sap_concur_list_expected_expenses` + +List expected expenses on a travel request (GET /travelrequest/v4/requests/\{requestUuid\}/expenses). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID | +| `userId` | string | No | User UUID acting on the request \(optional\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Array of expected expense objects. Each entry includes id, href, expenseType \{id,name\}, transactionDate, transactionAmount, postedAmount, approvedAmount, remainingAmount, businessPurpose, location, exchangeRate, allocations, tripData, parentRequest \{href, id\}, comments \{href, id\}. | + +### `sap_concur_list_expenses` + +List expenses on a report (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER, MANAGER, or PROXY | +| `reportId` | string | Yes | Expense report ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of expense summary entries \(ReportExpenseSummary\[\]\) | +| ↳ `expenseId` | string | Expense identifier | +| ↳ `expenseType` | json | Expense type \{id, name, code, isDeleted\} | +| ↳ `transactionDate` | string | Transaction date \(YYYY-MM-DD\) | +| ↳ `transactionAmount` | json | Transaction amount \{currencyCode, value\} | +| ↳ `postedAmount` | json | Posted amount | +| ↳ `approvedAmount` | json | Approved amount | +| ↳ `claimedAmount` | json | Claimed amount | +| ↳ `approverAdjustedAmount` | json | Approver-adjusted amount | +| ↳ `paymentType` | json | Payment type \{id, name, code\} | +| ↳ `vendor` | json | Vendor info | +| ↳ `location` | json | Location info | +| ↳ `allocationState` | string | Allocation state | +| ↳ `allocationSetId` | string | Allocation set identifier | +| ↳ `attendeeCount` | number | Attendee count | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `hasBlockingExceptions` | boolean | Has submission-blocking exceptions | +| ↳ `hasExceptions` | boolean | Has exceptions | +| ↳ `hasMissingReceiptDeclaration` | boolean | Has missing-receipt declaration | +| ↳ `isAutoCreated` | boolean | Auto-created | +| ↳ `isPersonalExpense` | boolean | Personal-expense flag | +| ↳ `isImageRequired` | boolean | Receipt image required | +| ↳ `isPaperReceiptRequired` | boolean | Paper receipt required | +| ↳ `imageCertificationStatus` | string | Receipt image certification status | +| ↳ `receiptImageId` | string | Receipt image identifier | +| ↳ `ereceiptImageId` | string | eReceipt image identifier | +| ↳ `ticketNumber` | string | Ticket number | +| ↳ `exchangeRate` | json | Exchange rate | +| ↳ `travelAllowance` | json | Travel allowance | +| ↳ `expenseSourceIdentifiers` | json | Expense source identifiers | +| ↳ `links` | array | HATEOAS links | + +### `sap_concur_list_expense_reports` + +List expense reports (GET /api/v3.0/expense/reports). Returns a v3 envelope with Items and NextPage. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(us, us2, eu, eu2, cn, emea — defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `user` | string | No | Filter by a specific user \(login id or user identifier\). | +| `submitDateBefore` | string | No | Filter to reports submitted on or before this date \(YYYY-MM-DD\) | +| `submitDateAfter` | string | No | Filter to reports submitted on or after this date \(YYYY-MM-DD\) | +| `paidDateBefore` | string | No | Filter to reports paid on or before this date \(YYYY-MM-DD\) | +| `paidDateAfter` | string | No | Filter to reports paid on or after this date \(YYYY-MM-DD\) | +| `modifiedDateBefore` | string | No | Filter to reports last modified on or before this date \(YYYY-MM-DD\) | +| `modifiedDateAfter` | string | No | Filter to reports last modified on or after this date \(YYYY-MM-DD\) | +| `createDateBefore` | string | No | Filter to reports created on or before this date \(YYYY-MM-DD\) | +| `createDateAfter` | string | No | Filter to reports created on or after this date \(YYYY-MM-DD\) | +| `approvalStatusCode` | string | No | Filter by approval status code \(e.g. A_NOTF, A_PEND, A_APPR\) | +| `paymentStatusCode` | string | No | Filter by payment status code | +| `currencyCode` | string | No | Filter by ISO currency code \(e.g. USD, EUR\) | +| `approverLoginID` | string | No | Filter by approver login ID | +| `limit` | number | No | Number of records per page \(default 25, max 100\) | +| `offset` | string | No | Opaque cursor token returned by a prior call \(NextPage\). | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Concur v3 expense reports envelope | +| ↳ `Items` | array | Array of report header objects | +| ↳ `ID` | string | Report ID | +| ↳ `Name` | string | Report name | +| ↳ `OwnerLoginID` | string | Owner login ID | +| ↳ `OwnerName` | string | Owner display name | +| ↳ `Total` | number | Report total | +| ↳ `TotalApprovedAmount` | number | Total approved amount | +| ↳ `TotalClaimedAmount` | number | Total claimed amount | +| ↳ `AmountDueEmployee` | number | Amount due employee | +| ↳ `CurrencyCode` | string | ISO currency code | +| ↳ `ApprovalStatusName` | string | Approval status name | +| ↳ `ApprovalStatusCode` | string | Approval status code | +| ↳ `PaymentStatusName` | string | Payment status name | +| ↳ `PaymentStatusCode` | string | Payment status code | +| ↳ `ApproverLoginID` | string | Approver login ID | +| ↳ `ApproverName` | string | Approver display name | +| ↳ `HasException` | boolean | Whether the report has any exception | +| ↳ `ReceiptsReceived` | boolean | Whether paper receipts were received | +| ↳ `CreateDate` | string | Creation date | +| ↳ `SubmitDate` | string | Submit date | +| ↳ `LastModifiedDate` | string | Last modified date | +| ↳ `PaidDate` | string | Paid date | +| ↳ `URI` | string | Self URI | +| ↳ `NextPage` | string | URI of the next page \(use as offset cursor\) | + +### `sap_concur_list_itineraries` + +List travel trips/itineraries (GET /api/travel/trip/v1.1). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `startDate` | string | No | Filter trips starting on/after this date \(YYYY-MM-DD\) | +| `endDate` | string | No | Filter trips ending on/before this date \(YYYY-MM-DD\) | +| `bookingType` | string | No | Filter by booking type \(air, car, hotel, rail, etc.\) | +| `useridType` | string | No | User identifier type \(login, xmlsyncid, uuid\) | +| `useridValue` | string | No | User identifier value \(paired with useridType\) | +| `itemsPerPage` | number | No | Items per page | +| `page` | number | No | 1-based page number | +| `includeMetadata` | boolean | No | Include paging metadata in the response | +| `includeCanceledTrips` | boolean | No | Include canceled trips in the result set | +| `createdAfterDate` | string | No | Only trips created after this date \(YYYY-MM-DD\) | +| `createdBeforeDate` | string | No | Only trips created before this date \(YYYY-MM-DD\) | +| `lastModifiedDate` | string | No | Only trips modified on/after this date \(YYYY-MM-DD\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Trips list payload \(Itinerary v1.1 ConnectResponse\) | +| ↳ `Metadata` | json | Paging metadata \(when includeMetadata=true\) | +| ↳ `Paging` | json | Pagination details | +| ↳ `TotalPages` | number | Total pages | +| ↳ `TotalItems` | number | Total items | +| ↳ `Page` | number | Current page | +| ↳ `ItemsPerPage` | number | Items per page | +| ↳ `PreviousPageURL` | string | Previous page URL | +| ↳ `NextPageURL` | string | Next page URL | +| ↳ `ItineraryInfoList` | array | List of itinerary summary records | +| ↳ `ItinLocator` | string | Trip locator \(trip ID\) | +| ↳ `ClientLocator` | string | Client trip locator | +| ↳ `ItinSourceName` | string | Booking source name | +| ↳ `BookedVia` | string | Booking channel | +| ↳ `TripName` | string | Trip name | +| ↳ `Status` | string | Trip status | +| ↳ `Description` | string | Trip description | +| ↳ `StartDateUtc` | string | Start \(UTC\) | +| ↳ `EndDateUtc` | string | End \(UTC\) | +| ↳ `StartDateLocal` | string | Start \(local\) | +| ↳ `EndDateLocal` | string | End \(local\) | +| ↳ `DateCreatedUtc` | string | Created \(UTC\) | +| ↳ `DateModifiedUtc` | string | Modified \(UTC\) | +| ↳ `DateBookedLocal` | string | Booked \(local\) | +| ↳ `UserLoginId` | string | Trip owner login id | +| ↳ `BookedByFirstName` | string | Booker first name | +| ↳ `BookedByLastName` | string | Booker last name | +| ↳ `IsPersonal` | boolean | Personal trip flag | + +### `sap_concur_list_lists` + +List custom lists (GET /list/v4/lists). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `page` | number | No | Page number \(1-based; page size is fixed at 100\) | +| `sortBy` | string | No | Sort field: name, levelcount, or listcategory | +| `sortDirection` | string | No | Sort direction: asc or desc | +| `value` | string | No | Filter by list name | +| `categoryType` | string | No | Filter by category type \(mapped to category.type query param\) | +| `isDeleted` | boolean | No | Include deleted lists | +| `levelCount` | number | No | Filter by number of levels | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Paginated lists collection | +| ↳ `content` | array | Lists in the current page | +| ↳ `id` | string | List UUID | +| ↳ `value` | string | Name of the list | +| ↳ `levelCount` | number | Number of levels in the list | +| ↳ `searchCriteria` | string | Search attribute \(TEXT or CODE\) | +| ↳ `displayFormat` | string | Display order \(\(CODE\) TEXT or TEXT \(CODE\)\) | +| ↳ `category` | json | List category | +| ↳ `id` | string | Category UUID | +| ↳ `type` | string | Category type | +| ↳ `isReadOnly` | boolean | Whether the list is read-only | +| ↳ `isDeleted` | boolean | Whether the list has been deleted | +| ↳ `managedBy` | string | Managing application or service identifier | +| ↳ `externalThreshold` | number | Threshold from where the level starts being external | +| ↳ `page` | json | Pagination metadata | +| ↳ `number` | number | Current page number | +| ↳ `size` | number | Items per page | +| ↳ `totalElements` | number | Total item count | +| ↳ `totalPages` | number | Total page count | +| ↳ `links` | array | Navigation links \(next, previous, first, last\) | +| ↳ `rel` | string | Link relation | +| ↳ `href` | string | Link URL | + +### `sap_concur_list_list_items` + +List the top-level items (children) for a custom list (GET /list/v4/lists/\{listId\}/children). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `listId` | string | Yes | List ID | +| `page` | number | No | Page number \(1-based; page size is fixed at 100\) | +| `sortBy` | string | No | Sort field: value or shortCode | +| `sortDirection` | string | No | Sort direction: asc or desc | +| `hasChildren` | boolean | No | Include only items that have children | +| `isDeleted` | boolean | No | Include deleted items | +| `shortCode` | string | No | Filter by short code | +| `value` | string | No | Filter by display value | +| `shortCodeOrValue` | string | No | Filter by short code OR value | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Paginated list items collection | +| ↳ `content` | array | List items in the current page | +| ↳ `id` | string | List item UUID | +| ↳ `code` | string | Long code format for the item | +| ↳ `shortCode` | string | Short code identifier | +| ↳ `value` | string | Display value of the item | +| ↳ `parentId` | string | Parent item UUID \(omitted for first-level items\) | +| ↳ `level` | number | Hierarchy level \(1 for root items\) | +| ↳ `isDeleted` | boolean | Deletion status across all containing lists | +| ↳ `lists` | array | Lists containing this item | +| ↳ `id` | string | List UUID | +| ↳ `hasChildren` | boolean | Whether this item has children in the list | +| ↳ `page` | json | Pagination metadata | +| ↳ `number` | number | Current page number | +| ↳ `size` | number | Items per page | +| ↳ `totalElements` | number | Total item count | +| ↳ `totalPages` | number | Total page count | +| ↳ `links` | array | Navigation links \(next, previous, first, last\) | +| ↳ `rel` | string | Link relation | +| ↳ `href` | string | Link URL | + +### `sap_concur_list_receipts` + +List receipts for a user (GET /receipts/v4/users/\{userId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of e-receipt objects | +| ↳ `id` | string | Receipt id | +| ↳ `userId` | string | Owner user UUID | +| ↳ `dateTimeReceived` | string | Timestamp the receipt was received | +| ↳ `receipt` | json | Structured receipt data | +| ↳ `image` | string | Receipt image URL or reference | +| ↳ `validationSchema` | string | Validation schema URI | +| ↳ `self` | string | Self URL | +| ↳ `template` | string | Template URL | + +### `sap_concur_list_report_comments` + +List comments on a report (GET /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/comments). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `includeAllComments` | boolean | No | Include comments from all expenses in the report \(default false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of report comment entries | +| ↳ `comment` | string | Comment text | +| ↳ `creationDate` | string | Comment creation timestamp \(ISO 8601\) | +| ↳ `expenseId` | string | Related expense entry ID | +| ↳ `isAuditorComment` | boolean | Whether the comment was added by an auditor | +| ↳ `isLatest` | boolean | Whether this is the latest comment | +| ↳ `createdForEmployeeId` | string | Employee ID the comment was created for | +| ↳ `author` | json | Comment author | +| ↳ `employeeId` | string | Employee identifier | +| ↳ `employeeUuid` | string | Employee UUID | +| ↳ `createdForEmployee` | json | Employee the comment was created for | +| ↳ `employeeId` | string | Employee identifier | +| ↳ `employeeUuid` | string | Employee UUID | +| ↳ `stepInstanceId` | string | Workflow step instance identifier | + +### `sap_concur_list_reports_to_approve` + +List expense reports awaiting approval (GET /expensereports/v4/users/\{userId\}/context/MANAGER/reportsToApprove). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Manager user UUID | +| `contextType` | string | No | Access context: must be MANAGER \(default\) | +| `sort` | string | No | Report field name to sort by \(e.g., reportDate\) | +| `order` | string | No | Sort direction: asc or desc | +| `includeDelegateApprovals` | boolean | No | Whether to include reports the caller can approve as a delegate | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of reports awaiting approval \(ReportToApprove\[\]\) | +| ↳ `reportId` | string | Unique report identifier | +| ↳ `name` | string | Report name | +| ↳ `reportDate` | string | Report date \(YYYY-MM-DD\) | +| ↳ `reportNumber` | string | User-friendly report number | +| ↳ `submitDate` | string | Submission timestamp \(ISO 8601 UTC\) | +| ↳ `approver` | json | Approver employee \{ employeeId, employeeUuid \} | +| ↳ `employee` | json | Report owner employee \{ employeeId, employeeUuid \} | +| ↳ `amountDueEmployee` | json | Amount due employee \{ value, currencyCode \} | +| ↳ `claimedAmount` | json | Total claimed amount \{ value, currencyCode \} | +| ↳ `totalApprovedAmount` | json | Total approved amount \{ value, currencyCode \} | +| ↳ `hasExceptions` | boolean | Whether the report has exceptions | +| ↳ `reportType` | string | Report creation method identifier | +| ↳ `links` | array | HATEOAS links | + +### `sap_concur_get_request_cash_advance` + +Get a single cash advance assigned to a travel request (GET /travelrequest/v4/cashadvances/\{cashAdvanceUuid\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `cashAdvanceUuid` | string | Yes | Cash advance UUID \(returned as part of a travel request\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Cash advance detail | +| ↳ `cashAdvanceId` | string | Unique cash advance identifier | +| ↳ `amountRequested` | json | Requested amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `amount` | number | Amount \(alias\) | +| ↳ `approvalStatus` | json | Approval status | +| ↳ `code` | string | Status code | +| ↳ `name` | string | Status name | +| ↳ `requestDate` | string | Request datetime \(ISO 8601\) | +| ↳ `exchangeRate` | json | Exchange rate | +| ↳ `value` | number | Rate value | +| ↳ `operation` | string | Multiply or divide | + +### `sap_concur_list_travel_profiles_summary` + +List travel profile summaries (GET /api/travelprofile/v2.0/summary). LastModifiedDate is required by Concur. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `lastModifiedDate` | string | Yes | Required UTC datetime in YYYY-MM-DDThh:mm:ss format | +| `page` | number | No | 1-based page number | +| `itemsPerPage` | number | No | Items per page \(max 200\) | +| `travelConfigs` | string | No | Comma-separated travel configuration ids | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Travel profile summary list payload \(Concur returns XML mapped to JSON\) | +| ↳ `Metadata` | json | Paging metadata | +| ↳ `Paging` | json | Pagination details | +| ↳ `TotalPages` | number | Total number of pages | +| ↳ `TotalItems` | number | Total number of items | +| ↳ `Page` | number | Current page | +| ↳ `ItemsPerPage` | number | Items per page | +| ↳ `PreviousPageURL` | string | URL to the previous page | +| ↳ `NextPageURL` | string | URL to the next page | +| ↳ `Data` | array | Array of travel profile summaries | +| ↳ `Status` | string | Status \(Active/Inactive\) | +| ↳ `LoginID` | string | Login identifier | +| ↳ `XmlProfileSyncID` | string | XML profile sync identifier | +| ↳ `ProfileLastModifiedUTC` | string | Last modified timestamp \(UTC\) | +| ↳ `RuleClass` | string | Travel rule class assigned to the profile | +| ↳ `TravelConfigID` | string | Travel configuration identifier | +| ↳ `UUID` | string | Profile UUID | +| ↳ `EmployeeID` | string | Employee ID | +| ↳ `CompanyID` | string | Company ID | + +### `sap_concur_list_travel_request_comments` + +List comments on a travel request (GET /travelrequest/v4/requests/\{requestUuid\}/comments). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | array | Array of comment entries | +| ↳ `author` | json | Comment author | +| ↳ `firstName` | string | Author first name | +| ↳ `lastName` | string | Author last name | +| ↳ `creationDateTime` | string | Comment creation timestamp \(ISO 8601\) | +| ↳ `isLatest` | boolean | Whether this is the latest comment | +| ↳ `value` | string | Comment text | + +### `sap_concur_list_travel_requests` + +List travel requests (GET /travelrequest/v4/requests). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `view` | string | No | View filter \(e.g., ALL, ACTIVE, PENDING, TOAPPROVE\) | +| `limit` | number | No | Max number of results per page | +| `start` | number | No | Page start cursor \(offset\) | +| `userId` | string | No | Filter by Concur user UUID | +| `approvedBefore` | string | No | ISO 8601 date — return requests approved before this date | +| `approvedAfter` | string | No | ISO 8601 date — return requests approved after this date | +| `modifiedBefore` | string | No | ISO 8601 date — return requests modified before this date | +| `modifiedAfter` | string | No | ISO 8601 date — return requests modified after this date | +| `sortField` | string | No | Field to sort by: startDate, approvalStatus, or requestId \(default startDate\) | +| `sortOrder` | string | No | Sort order: ASC or DESC \(default DESC\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Travel requests list payload | +| ↳ `data` | array | Array of travel request summaries | +| ↳ `id` | string | Travel request UUID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `requestId` | string | Public-facing request ID | +| ↳ `name` | string | Request name | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `comment` | string | Last attached comment | +| ↳ `creationDate` | string | Creation timestamp | +| ↳ `submitDate` | string | Last submission timestamp | +| ↳ `startDate` | string | Trip start date \(ISO 8601\) | +| ↳ `endDate` | string | Trip end date \(ISO 8601\) | +| ↳ `startTime` | string | Trip start time \(HH:mm\) | +| ↳ `approved` | boolean | Whether the request is approved | +| ↳ `pendingApproval` | boolean | Pending approval flag | +| ↳ `closed` | boolean | Closed flag | +| ↳ `everSentBack` | boolean | Ever-sent-back flag | +| ↳ `canceledPostApproval` | boolean | Canceled after approval flag | +| ↳ `approvalStatus` | json | Approval status | +| ↳ `code` | string | Status code \(NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK\) | +| ↳ `name` | string | Localized status name | +| ↳ `owner` | json | Travel request owner | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Owner first name | +| ↳ `lastName` | string | Owner last name | +| ↳ `approver` | json | Approver assigned to the request | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Approver first name | +| ↳ `lastName` | string | Approver last name | +| ↳ `type` | json | Request type | +| ↳ `code` | string | Request type code | +| ↳ `label` | string | Request type label | +| ↳ `totalApprovedAmount` | json | Total approved amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalPostedAmount` | json | Total posted amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalRemainingAmount` | json | Total remaining amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `expenses` | array | Resource links to expected expenses | +| ↳ `operations` | array | Pagination links \(next, prev, first, last\) | +| ↳ `rel` | string | Link relation | +| ↳ `href` | string | Link target | +| ↳ `method` | string | HTTP method | +| ↳ `name` | string | Link name | + +### `sap_concur_list_users` + +List Concur user identities (GET /profile/identity/v4.1/Users). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `count` | number | No | Max number of users to return \(default 100, max 1000\) | +| `cursor` | string | No | SCIM v4.1 pagination cursor returned by a prior call | +| `attributes` | string | No | Comma-separated list of attributes to include in the response | +| `excludedAttributes` | string | No | Comma-separated list of attributes to exclude from the response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | SCIM ListResponse with Resources array | + +### `sap_concur_move_travel_request` + +Move a travel request through workflow (POST /travelrequest/v4/requests/\{requestUuid\}/\{action\}). Valid actions: submit, recall, cancel, approve, sendback, close, reopen. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID | +| `action` | string | Yes | Workflow action: submit, recall, cancel, approve, sendback, close, reopen | +| `userId` | string | No | Optional Concur user UUID — required when impersonating another user | +| `body` | json | No | Optional payload \(e.g., \{ "comment": "..." \}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Workflow transition response payload | +| ↳ `id` | string | Travel request UUID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `approvalStatus` | json | Approval status after the workflow transition | +| ↳ `code` | string | Status code \(NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK\) | +| ↳ `name` | string | Localized status name | +| ↳ `approver` | json | Approver assigned after the transition | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Approver first name | +| ↳ `lastName` | string | Approver last name | +| ↳ `operations` | array | Available follow-up workflow actions | +| ↳ `rel` | string | Link relation | +| ↳ `href` | string | Link target | +| ↳ `method` | string | HTTP method | +| ↳ `name` | string | Link name | + +### `sap_concur_recall_expense_report` + +Recall a submitted expense report (PATCH /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/recall — supported contexts: TRAVELER, PROXY). No request body is required. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who owns the report | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID to recall | +| `body` | json | No | Optional body. Concur docs don't define a payload for this action; pass an empty object if uncertain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty \(204 No Content\) | + +### `sap_concur_remove_all_attendees` + +Remove all attendees from an expense (DELETE /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/expenses/\{expenseId\}/attendees). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty response body \(Concur returns 204 No Content\) | + +### `sap_concur_search_locations` + +Search Concur location reference data (GET /localities/v5/locations). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `searchText` | string | No | Free-text query \(city, airport, landmark, etc.\) | +| `locCode` | string | No | IATA / location code | +| `locationNameId` | string | No | Concur internal location name ID \(UUID\) | +| `locationNameKey` | number | No | Concur internal numeric location name key | +| `countryCode` | string | No | 2-letter ISO 3166-1 country code | +| `subdivisionCode` | string | No | ISO 3166-2:2007 country subdivision \(e.g. US-WA\) | +| `adminRegionId` | string | No | Administrative region ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Localities v5 search response | +| ↳ `locations` | array | Array of matching Location records | +| ↳ `id` | string | Location ID \(UUID\) | +| ↳ `code` | string | IATA / location code | +| ↳ `legacyKey` | number | Legacy numeric location key | +| ↳ `timeZoneOffset` | string | IANA timezone or UTC offset | +| ↳ `active` | boolean | Whether the location is active | +| ↳ `point` | json | Geographic coordinates | +| ↳ `latitude` | number | Latitude | +| ↳ `longitude` | number | Longitude | +| ↳ `names` | array | Localized location names | +| ↳ `id` | string | Name ID | +| ↳ `key` | number | Numeric name key | +| ↳ `locale` | string | Locale tag | +| ↳ `name` | string | Display name | +| ↳ `administrativeRegion` | json | Administrative region \(e.g., metro area\) | +| ↳ `id` | string | Region ID | +| ↳ `name` | string | Region name | +| ↳ `country` | json | Country reference | +| ↳ `id` | string | Country ID | +| ↳ `code` | string | ISO country code | +| ↳ `name` | string | Country name | +| ↳ `subDivision` | json | Country subdivision \(state/province\) | +| ↳ `id` | string | Subdivision ID | +| ↳ `code` | string | ISO subdivision code | +| ↳ `name` | string | Subdivision name | +| ↳ `links` | array | HATEOAS links | + +### `sap_concur_search_users` + +Search users via SCIM .search endpoint (POST /profile/identity/v4.1/Users/.search). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `body` | json | Yes | SCIM search request payload \(\{ schemas, attributes, filter, count, startIndex \}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | SCIM search ListResponse | + +### `sap_concur_send_back_expense_report` + +Send back an expense report to the employee (PATCH /expensereports/v4/reports/\{reportId\}/sendBack). Required body field: comment. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `reportId` | string | Yes | Expense report ID to send back | +| `body` | json | Yes | Request body — `comment` is required by Concur \(e.g., \{ "comment": "Missing receipt" \}\). Optional fields: `expectedStepCode`, `expectedStepSequence`. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty \(204 No Content\) | + +### `sap_concur_submit_expense_report` + +Submit an expense report into the workflow via Expense Report v4 (PATCH /expensereports/v4/users/\{userId\}/reports/\{reportId\}/submit). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who owns the report | +| `reportId` | string | Yes | Expense report ID to submit | +| `body` | json | No | Optional body. Concur docs don't define a payload for this action; pass an empty object if uncertain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty \(204 No Content\) | + +### `sap_concur_update_allocation` + +Update an allocation (PATCH /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\}/allocations/\{allocationId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID | +| `contextType` | string | Yes | Access context: TRAVELER or PROXY \(write requires expense.report.readwrite\) | +| `reportId` | string | Yes | Expense report ID | +| `allocationId` | string | Yes | Allocation ID to update | +| `body` | json | Yes | Fields to update on the allocation | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty body on success \(Concur returns 204 No Content\) | + +### `sap_concur_update_expected_expense` + +Update an expected expense (PUT /travelrequest/v4/expenses/\{expenseUuid\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `expenseUuid` | string | Yes | Expected expense UUID to update | +| `userId` | string | No | User UUID acting on the request \(required when using a Company JWT, optional otherwise\) | +| `body` | json | Yes | Fields to update on the expected expense | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Updated expected expense payload | +| ↳ `id` | string | Expected expense identifier | +| ↳ `href` | string | Self-link | +| ↳ `expenseType` | json | Expense type \{id, name\} | +| ↳ `transactionDate` | string | Transaction date | +| ↳ `transactionAmount` | json | Transaction amount \{value, currencyCode\} | +| ↳ `postedAmount` | json | Posted amount \{value, currencyCode\} | +| ↳ `approvedAmount` | json | Approved amount \{value, currencyCode\} | +| ↳ `remainingAmount` | json | Remaining amount on the expected expense | +| ↳ `businessPurpose` | string | Business purpose of the expense | +| ↳ `location` | json | Location \{id, name, city, countryCode, countrySubDivisionCode, iataCode, locationType\} | +| ↳ `exchangeRate` | json | Exchange rate \{value, operation\} | +| ↳ `allocations` | json | Budget allocations array | +| ↳ `tripData` | json | Trip data \{agencyBooked, selfBooked, tripType \(ONE_WAY\|ROUND_TRIP\), legs\[\{id, returnLeg, startDate, startTime, startLocationDetail, startLocation, endLocation, class \{code,value\}, travelExceptionReasonCodes\}\], segmentType \{category, code\}\} | +| ↳ `parentRequest` | json | Parent travel request resource link \{href, id\} | +| ↳ `comments` | json | Comments sub-resource link \{href, id\} | + +### `sap_concur_update_expense` + +Update an expense (PATCH /expensereports/v4/reports/\{reportId\}/expenses/\{expenseId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `reportId` | string | Yes | Expense report ID | +| `expenseId` | string | Yes | Expense ID to update | +| `body` | json | Yes | PATCH body. Allowed fields: businessPurpose \(string, max 64\), customData \(CustomData\[\]\), expenseSource \(required: EA\|MOB\|OTHER\|SE\|TA\|TR\|UI\), isExpenseRejected \(boolean\), isPaperReceiptReceived \(boolean\). | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty body on success \(HTTP 204 No Content\). Error details when status is non-2xx | + +### `sap_concur_update_expense_report` + +Update an unsubmitted expense report (PATCH /expensereports/v4/users/\{userId\}/context/\{contextType\}/reports/\{reportId\} — supported contexts: TRAVELER, PROXY). Body fields: businessPurpose, comment, customData, name, etc. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who owns the report | +| `contextType` | string | Yes | Access context: TRAVELER \(own report\) or PROXY \(editing on behalf of another user\) | +| `reportId` | string | Yes | Expense report ID to update | +| `body` | json | Yes | Fields to update on the report | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Empty \(204 No Content\) | + +### `sap_concur_update_list_item` + +Update a list item (PUT /list/v4/items/\{itemId\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `itemId` | string | Yes | List item UUID | +| `body` | json | Yes | List item payload. Required: shortCode, value. Other fields in the body are ignored. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Updated list item | +| ↳ `id` | string | List item UUID | +| ↳ `code` | string | Long code format for the item | +| ↳ `shortCode` | string | Short code identifier | +| ↳ `value` | string | Display value of the item | +| ↳ `parentId` | string | Parent item UUID \(omitted for first-level items\) | +| ↳ `level` | number | Hierarchy level \(1 for root items\) | +| ↳ `isDeleted` | boolean | Deletion status across all containing lists | +| ↳ `lists` | array | Lists containing this item | +| ↳ `id` | string | List UUID | +| ↳ `hasChildren` | boolean | Whether this item has children in the list | + +### `sap_concur_update_travel_request` + +Update a travel request (PUT /travelrequest/v4/requests/\{requestUuid\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `requestUuid` | string | Yes | Travel request UUID to update | +| `body` | json | Yes | Fields to update on the travel request | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Updated travel request payload | +| ↳ `id` | string | Travel request UUID | +| ↳ `href` | string | Resource hyperlink | +| ↳ `requestId` | string | Public-facing request ID \(4-6 alphanumeric characters\) | +| ↳ `name` | string | Request name | +| ↳ `businessPurpose` | string | Business purpose | +| ↳ `comment` | string | Last attached comment | +| ↳ `creationDate` | string | Creation timestamp | +| ↳ `lastModified` | string | Last modification timestamp | +| ↳ `submitDate` | string | Last submission timestamp | +| ↳ `startDate` | string | Trip start date \(ISO 8601\) | +| ↳ `endDate` | string | Trip end date \(ISO 8601\) | +| ↳ `startTime` | string | Trip start time \(HH:mm\) | +| ↳ `endTime` | string | Trip end time \(HH:mm\) | +| ↳ `approved` | boolean | Whether the request is approved | +| ↳ `pendingApproval` | boolean | Pending approval flag | +| ↳ `closed` | boolean | Closed flag | +| ↳ `everSentBack` | boolean | Ever-sent-back flag | +| ↳ `canceledPostApproval` | boolean | Canceled after approval flag | +| ↳ `approvalStatus` | json | Approval status | +| ↳ `code` | string | Status code \(NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK\) | +| ↳ `name` | string | Localized status name | +| ↳ `owner` | json | Travel request owner | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Owner first name | +| ↳ `lastName` | string | Owner last name | +| ↳ `approver` | json | Approver assigned to the request | +| ↳ `id` | string | User UUID | +| ↳ `firstName` | string | Approver first name | +| ↳ `lastName` | string | Approver last name | +| ↳ `policy` | json | Resource link to the applicable policy | +| ↳ `id` | string | Policy ID | +| ↳ `href` | string | Policy hyperlink | +| ↳ `type` | json | Request type | +| ↳ `code` | string | Request type code | +| ↳ `label` | string | Request type label | +| ↳ `mainDestination` | json | Main destination of the trip | +| ↳ `city` | string | City | +| ↳ `countryCode` | string | ISO country code | +| ↳ `countrySubDivisionCode` | string | ISO country sub-division code | +| ↳ `name` | string | Destination name | +| ↳ `totalApprovedAmount` | json | Total approved amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalPostedAmount` | json | Total posted amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `totalRemainingAmount` | json | Total remaining amount | +| ↳ `value` | number | Amount value | +| ↳ `currency` | string | Currency code | +| ↳ `operations` | array | Available workflow actions | + +### `sap_concur_update_user` + +Patch a user identity (PATCH /profile/identity/v4.1/Users/\{id\}). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userUuid` | string | Yes | User UUID to update | +| `body` | json | Yes | SCIM PATCH operations payload \(\{ schemas, Operations: \[...\] \}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Updated SCIM User payload | + +### `sap_concur_upload_receipt_image` + +Upload an image-only receipt (POST /receipts/v4/users/\{userId\}/image-only-receipts). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `datacenter` | string | No | Concur datacenter base URL \(defaults to us.api.concursolutions.com\) | +| `grantType` | string | No | OAuth grant type: client_credentials \(default\) or password | +| `clientId` | string | Yes | Concur OAuth client ID | +| `clientSecret` | string | Yes | Concur OAuth client secret | +| `username` | string | No | Username \(only for password grant\) | +| `password` | string | No | Password \(only for password grant\) | +| `companyUuid` | string | No | Company UUID for multi-company access tokens | +| `userId` | string | Yes | Concur user UUID who owns the receipt | +| `receipt` | json | Yes | Receipt image file \(UserFile reference\). Supported formats: PDF, PNG, JPEG, GIF, TIFF | +| `forwardId` | string | No | Optional client-supplied dedup id \(max 40 chars\). Sent as the concur-forwardid header. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | HTTP status code returned by Concur | +| `data` | json | Image-only receipt upload response \(HTTP 202 Accepted; Location and Link response headers exposed in body\) | +| ↳ `location` | string | Location header URL for the new receipt image \(e.g. /receipts/v4/images/\{receiptId\}\) | +| ↳ `link` | string | Link header URL pointing to /receipts/v4/status/\{receiptId\} | + + diff --git a/apps/docs/content/docs/en/tools/sap_s4hana.mdx b/apps/docs/content/docs/en/tools/sap_s4hana.mdx index 0c8aaf5c745..bc9bb49302a 100644 --- a/apps/docs/content/docs/en/tools/sap_s4hana.mdx +++ b/apps/docs/content/docs/en/tools/sap_s4hana.mdx @@ -62,10 +62,10 @@ List business partners from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessP | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -84,7 +84,22 @@ List business partners from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_BusinessP | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_BusinessPartner entities | +| `data` | json | OData v2 envelope `\{ d: \{ results: \[...\], __count?, __next? \} \}`. Properties listed below describe each element of `data.d.results`. | +| ↳ `BusinessPartner` | string | Business partner key \(up to 10 chars\) | +| ↳ `BusinessPartnerFullName` | string | Full name \(concatenated first/last or organization name\) | +| ↳ `BusinessPartnerCategory` | string | "1" Person, "2" Organization, "3" Group | +| ↳ `BusinessPartnerGrouping` | string | Grouping / number range \(tenant-configured\) | +| ↳ `BusinessPartnerType` | string | Business partner type \(tenant-configured\) | +| ↳ `BusinessPartnerUUID` | string | GUID identifier for the business partner | +| ↳ `BusinessPartnerIsBlocked` | boolean | Whether the business partner is centrally blocked | +| ↳ `FirstName` | string | First name \(Person\) | +| ↳ `LastName` | string | Last name \(Person\) | +| ↳ `OrganizationBPName1` | string | Organization name line 1 | +| ↳ `SearchTerm1` | string | Search term 1 | +| ↳ `CreationDate` | string | Date the partner was created \(OData /Date\(...\)/ literal\) | +| ↳ `CreatedByUser` | string | User who created the business partner | +| ↳ `LastChangeDate` | string | Date of last change \(OData /Date\(...\)/ literal\) | +| ↳ `LastChangedByUser` | string | User who last changed the business partner | ### `sap_s4hana_get_business_partner` @@ -94,10 +109,10 @@ Retrieve a single business partner by BusinessPartner key from SAP S/4HANA Cloud | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -113,7 +128,24 @@ Retrieve a single business partner by BusinessPartner key from SAP S/4HANA Cloud | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_BusinessPartner entity | +| `data` | json | A_BusinessPartner entity \(under d in OData v2\) | +| ↳ `BusinessPartner` | string | Business partner key \(up to 10 chars\) | +| ↳ `BusinessPartnerFullName` | string | Full name \(concatenated first/last or organization name\) | +| ↳ `BusinessPartnerCategory` | string | "1" Person, "2" Organization, "3" Group | +| ↳ `BusinessPartnerGrouping` | string | Grouping / number range \(tenant-configured\) | +| ↳ `BusinessPartnerType` | string | Business partner type \(tenant-configured\) | +| ↳ `BusinessPartnerUUID` | string | GUID identifier for the business partner | +| ↳ `BusinessPartnerIsBlocked` | boolean | Whether the business partner is centrally blocked | +| ↳ `FirstName` | string | First name \(Person\) | +| ↳ `LastName` | string | Last name \(Person\) | +| ↳ `OrganizationBPName1` | string | Organization name line 1 | +| ↳ `CorrespondenceLanguage` | string | Correspondence language \(2-char code, e.g. "EN"\) | +| ↳ `SearchTerm1` | string | Search term 1 | +| ↳ `SearchTerm2` | string | Search term 2 | +| ↳ `CreationDate` | string | Date the partner was created \(OData /Date\(...\)/ literal\) | +| ↳ `CreatedByUser` | string | User who created the business partner | +| ↳ `LastChangeDate` | string | Date of last change \(OData /Date\(...\)/ literal\) | +| ↳ `LastChangedByUser` | string | User who last changed the business partner | ### `sap_s4hana_create_business_partner` @@ -123,10 +155,10 @@ Create a business partner in SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Business | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -144,21 +176,33 @@ Create a business partner in SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Business | Parameter | Type | Description | | --------- | ---- | ----------- | -| `status` | number | HTTP status code returned by SAP | -| `data` | json | Created A_BusinessPartner entity | +| `status` | number | HTTP status code returned by SAP \(201 on success\) | +| `data` | json | Created A_BusinessPartner entity \(under d in OData v2\) | +| ↳ `BusinessPartner` | string | Generated business partner key \(up to 10 chars\) | +| ↳ `BusinessPartnerFullName` | string | Full name \(concatenated first/last or organization name\) | +| ↳ `BusinessPartnerCategory` | string | "1" Person, "2" Organization, "3" Group | +| ↳ `BusinessPartnerGrouping` | string | Grouping / number range used to assign the key | +| ↳ `BusinessPartnerType` | string | Business partner type \(tenant-configured\) | +| ↳ `BusinessPartnerUUID` | string | GUID identifier for the business partner | +| ↳ `FirstName` | string | First name \(Person\) | +| ↳ `LastName` | string | Last name \(Person\) | +| ↳ `OrganizationBPName1` | string | Organization name line 1 | +| ↳ `CreationDate` | string | Date the partner was created \(OData /Date\(...\)/ literal\) | +| ↳ `CreatedByUser` | string | User who created the business partner | +| ↳ `LastChangeDate` | string | Date of last change \(OData /Date\(...\)/ literal\) | ### `sap_s4hana_update_business_partner` -Update fields on an A_BusinessPartner entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. +Update fields on an A_BusinessPartner entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. Deep updates on nested associations (e.g. to_BusinessPartnerAddress) are not supported by SAP (KBA 2833338) — use the dedicated child endpoints. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -175,6 +219,15 @@ Update fields on an A_BusinessPartner entity in SAP S/4HANA Cloud (API_BUSINESS_ | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | | `data` | json | Null on 204 success, or updated A_BusinessPartner entity if SAP returns one | +| ↳ `BusinessPartner` | string | Business partner key | +| ↳ `BusinessPartnerFullName` | string | Full name \(concatenated first/last or organization name\) | +| ↳ `BusinessPartnerCategory` | string | "1" Person, "2" Organization, "3" Group | +| ↳ `BusinessPartnerGrouping` | string | Grouping / number range | +| ↳ `FirstName` | string | First name \(Person\) | +| ↳ `LastName` | string | Last name \(Person\) | +| ↳ `OrganizationBPName1` | string | Organization name line 1 | +| ↳ `LastChangeDate` | string | Date of last change \(OData /Date\(...\)/ literal\) | +| ↳ `LastChangedByUser` | string | User who last changed the business partner | ### `sap_s4hana_list_customers` @@ -184,10 +237,10 @@ List customers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Customer) with op | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -206,7 +259,30 @@ List customers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Customer) with op | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_Customer entities | +| `data` | json | Array of A_Customer entities, or `\{ results, __count?, __next? \}` when pagination metadata is present \(proxy unwraps the OData v2 `d` envelope\). Properties below describe each customer item. | +| ↳ `Customer` | string | Customer key \(up to 10 characters\) | +| ↳ `CustomerName` | string | Name of customer | +| ↳ `CustomerFullName` | string | Full name of the customer | +| ↳ `CustomerAccountGroup` | string | Customer account group | +| ↳ `CustomerClassification` | string | Customer classification code | +| ↳ `CustomerCorporateGroup` | string | Corporate group code | +| ↳ `AuthorizationGroup` | string | Authorization group | +| ↳ `Supplier` | string | Linked supplier account number | +| ↳ `FiscalAddress` | string | Fiscal address ID | +| ↳ `Industry` | string | Industry key | +| ↳ `NielsenRegion` | string | Nielsen ID | +| ↳ `ResponsibleType` | string | Responsible type | +| ↳ `NFPartnerIsNaturalPerson` | string | Natural person indicator | +| ↳ `InternationalLocationNumber1` | string | International location number 1 | +| ↳ `TaxNumberType` | string | Tax number type | +| ↳ `VATRegistration` | string | VAT registration number | +| ↳ `DeletionIndicator` | boolean | Central deletion flag | +| ↳ `OrderIsBlockedForCustomer` | string | Central order block reason code | +| ↳ `PostingIsBlocked` | boolean | Central posting block flag | +| ↳ `DeliveryIsBlocked` | string | Central delivery block reason code | +| ↳ `BillingIsBlockedForCustomer` | string | Central billing block reason code | +| ↳ `CreationDate` | string | Creation date \(OData v2 epoch\) | +| ↳ `CreatedByUser` | string | User who created the customer | ### `sap_s4hana_get_customer` @@ -216,10 +292,10 @@ Retrieve a single customer by Customer key from SAP S/4HANA Cloud (API_BUSINESS_ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -235,20 +311,43 @@ Retrieve a single customer by Customer key from SAP S/4HANA Cloud (API_BUSINESS_ | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_Customer entity | +| `data` | object | A_Customer entity | +| ↳ `Customer` | string | Customer key \(up to 10 characters\) | +| ↳ `CustomerName` | string | Name of customer | +| ↳ `CustomerFullName` | string | Full name of the customer | +| ↳ `CustomerAccountGroup` | string | Customer account group | +| ↳ `CustomerClassification` | string | Customer classification code | +| ↳ `CustomerCorporateGroup` | string | Corporate group code | +| ↳ `AuthorizationGroup` | string | Authorization group | +| ↳ `Supplier` | string | Linked supplier account number | +| ↳ `FiscalAddress` | string | Fiscal address ID | +| ↳ `Industry` | string | Industry key | +| ↳ `NielsenRegion` | string | Nielsen ID | +| ↳ `ResponsibleType` | string | Responsible type | +| ↳ `NFPartnerIsNaturalPerson` | string | Natural person indicator | +| ↳ `InternationalLocationNumber1` | string | International location number 1 | +| ↳ `TaxNumberType` | string | Tax number type | +| ↳ `VATRegistration` | string | VAT registration number | +| ↳ `DeletionIndicator` | boolean | Central deletion flag | +| ↳ `OrderIsBlockedForCustomer` | string | Central order block reason code | +| ↳ `PostingIsBlocked` | boolean | Central posting block flag | +| ↳ `DeliveryIsBlocked` | string | Central delivery block reason code | +| ↳ `BillingIsBlockedForCustomer` | string | Central billing block reason code | +| ↳ `CreationDate` | string | Creation date \(OData v2 epoch\) | +| ↳ `CreatedByUser` | string | User who created the customer | ### `sap_s4hana_update_customer` -Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. A_Customer PATCH is limited to modifiable fields such as OrderIsBlockedForCustomer, DeliveryIsBlock, BillingIsBlockedForCustomer, PostingIsBlocked, and DeletionIndicator. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. +Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. A_Customer is limited to modifiable fields such as OrderIsBlockedForCustomer, DeliveryIsBlocked, BillingIsBlockedForCustomer (Edm.String reason codes like #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -256,7 +355,7 @@ Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER | `username` | string | No | Username for HTTP Basic auth | | `password` | string | No | Password for HTTP Basic auth | | `customer` | string | Yes | Customer key to update \(string, up to 10 characters\) | -| `body` | json | Yes | JSON object with A_Customer fields to update \(e.g., \{"OrderIsBlockedForCustomer":true,"DeletionIndicator":false\}\) | +| `body` | json | Yes | JSON object with A_Customer fields to update \(e.g., \{"OrderIsBlockedForCustomer":"01","DeletionIndicator":false\}\). Block-reason fields are Edm.String codes, not booleans. | | `ifMatch` | string | No | If-Match ETag for optimistic concurrency. Defaults to "*" \(unconditional\). | #### Output @@ -264,7 +363,15 @@ Update fields on an A_Customer entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on 204 success, or updated A_Customer entity if SAP returns one | +| `data` | object | Null on 204 success, or updated A_Customer entity if SAP returns one | +| ↳ `Customer` | string | Customer key \(up to 10 characters\) | +| ↳ `CustomerName` | string | Name of customer | +| ↳ `CustomerAccountGroup` | string | Customer account group | +| ↳ `DeletionIndicator` | boolean | Central deletion flag | +| ↳ `OrderIsBlockedForCustomer` | string | Central order block reason code | +| ↳ `PostingIsBlocked` | boolean | Central posting block flag | +| ↳ `DeliveryIsBlocked` | string | Central delivery block reason code | +| ↳ `BillingIsBlockedForCustomer` | string | Central billing block reason code | ### `sap_s4hana_list_suppliers` @@ -274,10 +381,10 @@ List suppliers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Supplier) with op | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -296,7 +403,47 @@ List suppliers from SAP S/4HANA Cloud (API_BUSINESS_PARTNER, A_Supplier) with op | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_Supplier entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_Supplier entities | +| ↳ `Supplier` | string | Supplier key \(up to 10 characters\) | +| ↳ `AlternativePayeeAccountNumber` | string | Account number of the alternative payee | +| ↳ `AuthorizationGroup` | string | Authorization group | +| ↳ `BusinessPartner` | string | Linked BusinessPartner key | +| ↳ `BR_TaxIsSplit` | boolean | Brazil-specific tax split flag | +| ↳ `CreatedByUser` | string | User who created the supplier | +| ↳ `CreationDate` | string | Creation date \(OData v2 epoch\) | +| ↳ `Customer` | string | Linked customer key \(if any\) | +| ↳ `DeletionIndicator` | boolean | Central deletion flag | +| ↳ `BirthDate` | string | Date of birth \(OData v2 epoch\) | +| ↳ `ConcatenatedInternationalLocNo` | string | Concatenated international location number | +| ↳ `FiscalAddress` | string | Fiscal address number | +| ↳ `Industry` | string | Industry key | +| ↳ `InternationalLocationNumber1` | string | International location number, part 1 | +| ↳ `InternationalLocationNumber2` | string | International location number, part 2 | +| ↳ `InternationalLocationNumber3` | string | International location number, part 3 | +| ↳ `IsNaturalPerson` | boolean | Indicates whether the supplier is a natural person | +| ↳ `PaymentIsBlockedForSupplier` | boolean | Payment block flag | +| ↳ `PostingIsBlocked` | boolean | Posting block flag | +| ↳ `PurchasingIsBlocked` | boolean | Purchasing block flag | +| ↳ `ResponsibleType` | string | Type of business \(Brazil\) | +| ↳ `SupplierAccountGroup` | string | Supplier account group | +| ↳ `SupplierCorporateGroup` | string | Corporate group identifier | +| ↳ `SupplierFullName` | string | Full name of the supplier | +| ↳ `SupplierName` | string | Supplier name | +| ↳ `SupplierProcurementBlock` | string | Procurement block at supplier level | +| ↳ `SuplrProofOfDelivRlvtCode` | string | Proof of delivery relevance code | +| ↳ `SuplrQltyInProcmtCertfnValidTo` | string | Quality certification validity end date \(OData v2 epoch\) | +| ↳ `SuplrQualityManagementSystem` | string | Quality management system of the supplier | +| ↳ `TaxNumber1` | string | Tax number 1 | +| ↳ `TaxNumber2` | string | Tax number 2 | +| ↳ `TaxNumber3` | string | Tax number 3 | +| ↳ `TaxNumber4` | string | Tax number 4 | +| ↳ `TaxNumber5` | string | Tax number 5 | +| ↳ `TaxNumberResponsible` | string | Tax number of responsible party | +| ↳ `TaxNumberType` | string | Tax number type | +| ↳ `VATRegistration` | string | VAT registration number | +| ↳ `__next` | string | OData skiptoken URL for next page | ### `sap_s4hana_get_supplier` @@ -306,10 +453,10 @@ Retrieve a single supplier by Supplier key from SAP S/4HANA Cloud (API_BUSINESS_ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -325,20 +472,58 @@ Retrieve a single supplier by Supplier key from SAP S/4HANA Cloud (API_BUSINESS_ | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_Supplier entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_Supplier entity | +| ↳ `Supplier` | string | Supplier key \(up to 10 characters\) | +| ↳ `AlternativePayeeAccountNumber` | string | Account number of the alternative payee | +| ↳ `AuthorizationGroup` | string | Authorization group | +| ↳ `BusinessPartner` | string | Linked BusinessPartner key | +| ↳ `BR_TaxIsSplit` | boolean | Brazil-specific tax split flag | +| ↳ `CreatedByUser` | string | User who created the supplier | +| ↳ `CreationDate` | string | Creation date \(OData v2 epoch\) | +| ↳ `Customer` | string | Linked customer key \(if any\) | +| ↳ `DeletionIndicator` | boolean | Central deletion flag | +| ↳ `BirthDate` | string | Date of birth \(OData v2 epoch\) | +| ↳ `ConcatenatedInternationalLocNo` | string | Concatenated international location number | +| ↳ `FiscalAddress` | string | Fiscal address number | +| ↳ `Industry` | string | Industry key | +| ↳ `InternationalLocationNumber1` | string | International location number, part 1 | +| ↳ `InternationalLocationNumber2` | string | International location number, part 2 | +| ↳ `InternationalLocationNumber3` | string | International location number, part 3 | +| ↳ `IsNaturalPerson` | boolean | Indicates whether the supplier is a natural person | +| ↳ `PaymentIsBlockedForSupplier` | boolean | Payment block flag | +| ↳ `PostingIsBlocked` | boolean | Posting block flag | +| ↳ `PurchasingIsBlocked` | boolean | Purchasing block flag | +| ↳ `ResponsibleType` | string | Type of business \(Brazil\) | +| ↳ `SupplierAccountGroup` | string | Supplier account group | +| ↳ `SupplierCorporateGroup` | string | Corporate group identifier | +| ↳ `SupplierFullName` | string | Full name of the supplier | +| ↳ `SupplierName` | string | Supplier name | +| ↳ `SupplierProcurementBlock` | string | Procurement block at supplier level | +| ↳ `SuplrProofOfDelivRlvtCode` | string | Proof of delivery relevance code | +| ↳ `SuplrQltyInProcmtCertfnValidTo` | string | Quality certification validity end date \(OData v2 epoch\) | +| ↳ `SuplrQualityManagementSystem` | string | Quality management system of the supplier | +| ↳ `TaxNumber1` | string | Tax number 1 | +| ↳ `TaxNumber2` | string | Tax number 2 | +| ↳ `TaxNumber3` | string | Tax number 3 | +| ↳ `TaxNumber4` | string | Tax number 4 | +| ↳ `TaxNumber5` | string | Tax number 5 | +| ↳ `TaxNumberResponsible` | string | Tax number of responsible party | +| ↳ `TaxNumberType` | string | Tax number type | +| ↳ `VATRegistration` | string | VAT registration number | ### `sap_s4hana_update_supplier` -Update fields on an A_Supplier entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). PATCH only sends the fields you provide; existing values are preserved. A_Supplier PATCH is limited to modifiable fields such as PostingIsBlocked, PurchasingIsBlocked, PaymentIsBlockedForSupplier, DeletionIndicator, and SupplierAccountGroup. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. +Update fields on an A_Supplier entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. A_Supplier is limited to modifiable fields such as PostingIsBlocked, PurchasingIsBlocked, PaymentIsBlockedForSupplier, DeletionIndicator, and SupplierAccountGroup; company-code/purchasing-org segments must be updated via the `to_SupplierCompany` / `to_SupplierPurchasingOrg` deep-update endpoints. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -354,7 +539,16 @@ Update fields on an A_Supplier entity in SAP S/4HANA Cloud (API_BUSINESS_PARTNER | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on 204 success, or updated A_Supplier entity if SAP returns one | +| `data` | json | Null on 204 success, or OData v2 envelope with updated entity at output.data.d when SAP returns a representation | +| ↳ `d` | json | A_Supplier entity \(when SAP returns a representation\) | +| ↳ `Supplier` | string | Supplier key \(up to 10 characters\) | +| ↳ `SupplierName` | string | Supplier name | +| ↳ `SupplierAccountGroup` | string | Supplier account group | +| ↳ `BusinessPartner` | string | Linked BusinessPartner key | +| ↳ `PaymentIsBlockedForSupplier` | boolean | Payment block flag | +| ↳ `PostingIsBlocked` | boolean | Posting block flag | +| ↳ `PurchasingIsBlocked` | boolean | Purchasing block flag | +| ↳ `DeletionIndicator` | boolean | Central deletion flag | ### `sap_s4hana_list_sales_orders` @@ -364,10 +558,10 @@ List sales orders from SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) wit | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -386,7 +580,26 @@ List sales orders from SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) wit | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_SalesOrder entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_SalesOrder entities | +| ↳ `SalesOrder` | string | Sales order number | +| ↳ `SalesOrderType` | string | Sales document type \(e.g., OR\) | +| ↳ `SalesOrganization` | string | Sales organization | +| ↳ `DistributionChannel` | string | Distribution channel | +| ↳ `OrganizationDivision` | string | Division | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `TotalNetAmount` | string | Total net amount | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `SalesOrderDate` | string | Sales order date \(OData /Date\(ms\)/\) | +| ↳ `RequestedDeliveryDate` | string | Requested delivery date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDate` | string | Last change date \(OData /Date\(ms\)/\) | +| ↳ `PurchaseOrderByCustomer` | string | Customer purchase order reference | +| ↳ `OverallSDProcessStatus` | string | Overall sales document process status | +| ↳ `OverallTotalDeliveryStatus` | string | Overall total delivery status | +| ↳ `OverallSDDocumentRejectionSts` | string | Overall sales document rejection status | +| ↳ `__next` | string | OData skiptoken URL for next page | ### `sap_s4hana_get_sales_order` @@ -396,10 +609,10 @@ Retrieve a single sales order by SalesOrder key from SAP S/4HANA Cloud (API_SALE | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -415,7 +628,27 @@ Retrieve a single sales order by SalesOrder key from SAP S/4HANA Cloud (API_SALE | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_SalesOrder entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_SalesOrder entity | +| ↳ `SalesOrder` | string | Sales order number | +| ↳ `SalesOrderType` | string | Sales document type | +| ↳ `SalesOrganization` | string | Sales organization | +| ↳ `DistributionChannel` | string | Distribution channel | +| ↳ `OrganizationDivision` | string | Division | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `PurchaseOrderByCustomer` | string | Customer purchase order reference | +| ↳ `SalesOrderDate` | string | Sales order date \(OData /Date\(ms\)/\) | +| ↳ `RequestedDeliveryDate` | string | Requested delivery date \(OData /Date\(ms\)/\) | +| ↳ `PricingDate` | string | Pricing date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDate` | string | Last change date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDateTime` | string | Last change timestamp \(OData /Date\(ms\)/\) | +| ↳ `TotalNetAmount` | string | Total net amount | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `CreationDate` | string | Creation date | +| ↳ `OverallSDProcessStatus` | string | Overall sales document process status | +| ↳ `OverallTotalDeliveryStatus` | string | Overall total delivery status | +| ↳ `OverallSDDocumentRejectionSts` | string | Overall sales document rejection status | +| ↳ `to_Item` | json | Sales order items \(when $expand=to_Item\) | ### `sap_s4hana_create_sales_order` @@ -425,10 +658,10 @@ Create a sales order in SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) wi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -447,21 +680,34 @@ Create a sales order in SAP S/4HANA Cloud (API_SALES_ORDER_SRV, A_SalesOrder) wi | Parameter | Type | Description | | --------- | ---- | ----------- | -| `status` | number | HTTP status code returned by SAP | -| `data` | json | Created A_SalesOrder entity \(with deep-inserted items if expanded by SAP\) | +| `status` | number | HTTP status code returned by SAP \(201 on create\) | +| `data` | json | OData v2 response envelope; created entity at output.data.d | +| ↳ `d` | json | Created A_SalesOrder entity | +| ↳ `SalesOrder` | string | Newly assigned sales order number | +| ↳ `SalesOrderType` | string | Sales document type | +| ↳ `SalesOrganization` | string | Sales organization | +| ↳ `DistributionChannel` | string | Distribution channel | +| ↳ `OrganizationDivision` | string | Division | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `TotalNetAmount` | string | Total net amount | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `CreationDate` | string | Creation date | +| ↳ `OverallSDProcessStatus` | string | Overall sales document process status | +| ↳ `OverallTotalDeliveryStatus` | string | Overall total delivery status | +| ↳ `to_Item` | json | Deep-inserted sales order items as returned by SAP | ### `sap_s4hana_update_sales_order` -Update fields on an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. +Update fields on an A_SalesOrder header in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. Header-only — deep updates to to_Item / to_Partner / to_PricingElement navigations are not supported (see SAP KBA 2833338); use A_SalesOrderItem operations for line-level changes. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -477,7 +723,13 @@ Update fields on an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SR | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on 204 success, or updated A_SalesOrder entity if SAP returns one | +| `data` | json | Null on 204 success; otherwise OData v2 envelope with the updated entity at output.data.d | +| ↳ `d` | json | Updated A_SalesOrder entity \(when SAP returns one\) | +| ↳ `SalesOrder` | string | Sales order number | +| ↳ `SalesOrderType` | string | Sales document type | +| ↳ `PurchaseOrderByCustomer` | string | Customer purchase order reference | +| ↳ `OverallSDProcessStatus` | string | Overall sales document process status | +| ↳ `OverallTotalDeliveryStatus` | string | Overall total delivery status | ### `sap_s4hana_delete_sales_order` @@ -487,10 +739,10 @@ Delete an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). Only o | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -505,7 +757,7 @@ Delete an A_SalesOrder entity in SAP S/4HANA Cloud (API_SALES_ORDER_SRV). Only o | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on successful deletion | +| `data` | json | Null on successful deletion \(SAP returns 204 No Content\) | ### `sap_s4hana_list_outbound_deliveries` @@ -515,10 +767,10 @@ List outbound deliveries from SAP S/4HANA Cloud (API_OUTBOUND_DELIVERY_SRV;v=000 | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -537,7 +789,27 @@ List outbound deliveries from SAP S/4HANA Cloud (API_OUTBOUND_DELIVERY_SRV;v=000 | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_OutbDeliveryHeader entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_OutbDeliveryHeader entities | +| ↳ `DeliveryDocument` | string | Outbound delivery number | +| ↳ `DeliveryDocumentType` | string | Delivery document type \(e.g., LF\) | +| ↳ `SDDocumentCategory` | string | SD document category \(e.g., J = outbound delivery\) | +| ↳ `ShippingPoint` | string | Shipping point | +| ↳ `ShippingType` | string | Shipping type | +| ↳ `ShipToParty` | string | Ship-to business partner | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `DeliveryDate` | string | Delivery date \(Edm.DateTime\) | +| ↳ `ActualGoodsMovementDate` | string | Actual goods issue date \(Edm.DateTime\) | +| ↳ `PlannedGoodsIssueDate` | string | Planned goods issue date \(Edm.DateTime\) | +| ↳ `OverallSDProcessStatus` | string | Overall SD process \(delivery\) status | +| ↳ `OverallGoodsMovementStatus` | string | Overall goods movement status | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `DocumentDate` | string | Document date \(Edm.DateTime\) | +| ↳ `CreationDate` | string | Creation date \(Edm.DateTime\) | +| ↳ `LastChangeDate` | string | Last change date \(Edm.DateTime\) | +| ↳ `__next` | string | OData skiptoken URL for next page | +| ↳ `__count` | string | Total count when $inlinecount=allpages is used | ### `sap_s4hana_get_outbound_delivery` @@ -547,10 +819,10 @@ Retrieve a single outbound delivery by DeliveryDocument key from SAP S/4HANA Clo | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -566,7 +838,26 @@ Retrieve a single outbound delivery by DeliveryDocument key from SAP S/4HANA Clo | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_OutbDeliveryHeader entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_OutbDeliveryHeader entity | +| ↳ `DeliveryDocument` | string | Outbound delivery number | +| ↳ `DeliveryDocumentType` | string | Delivery document type | +| ↳ `SDDocumentCategory` | string | SD document category \(e.g., J = outbound delivery\) | +| ↳ `ShippingPoint` | string | Shipping point | +| ↳ `ShippingType` | string | Shipping type | +| ↳ `ShipToParty` | string | Ship-to business partner | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `DeliveryDate` | string | Delivery date \(Edm.DateTime\) | +| ↳ `ActualGoodsMovementDate` | string | Actual goods issue date \(Edm.DateTime\) | +| ↳ `PlannedGoodsIssueDate` | string | Planned goods issue date \(Edm.DateTime\) | +| ↳ `OverallSDProcessStatus` | string | Overall SD process \(delivery\) status | +| ↳ `OverallGoodsMovementStatus` | string | Overall goods movement status | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `DocumentDate` | string | Document date \(Edm.DateTime\) | +| ↳ `CreationDate` | string | Creation date \(Edm.DateTime\) | +| ↳ `LastChangeDate` | string | Last change date \(Edm.DateTime\) | +| ↳ `to_DeliveryDocumentItem` | json | Delivery items \(when $expand=to_DeliveryDocumentItem\) | +| ↳ `to_DeliveryDocumentPartner` | json | Delivery partners \(when $expand=to_DeliveryDocumentPartner\) | ### `sap_s4hana_list_inbound_deliveries` @@ -576,10 +867,10 @@ List inbound deliveries from SAP S/4HANA Cloud (API_INBOUND_DELIVERY_SRV;v=0002, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -598,7 +889,25 @@ List inbound deliveries from SAP S/4HANA Cloud (API_INBOUND_DELIVERY_SRV;v=0002, | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_InbDeliveryHeader entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_InbDeliveryHeader entities | +| ↳ `DeliveryDocument` | string | Inbound delivery number | +| ↳ `DeliveryDocumentType` | string | Delivery document type \(e.g., EL\) | +| ↳ `SDDocumentCategory` | string | SD document category \(e.g., 7 = inbound delivery\) | +| ↳ `ReceivingPlant` | string | Receiving plant | +| ↳ `Supplier` | string | Supplier business partner | +| ↳ `ShipToParty` | string | Ship-to business partner | +| ↳ `DeliveryDate` | string | Delivery date \(Edm.DateTime\) | +| ↳ `ActualGoodsMovementDate` | string | Actual goods movement \(receipt\) date \(Edm.DateTime\) | +| ↳ `PlannedGoodsMovementDate` | string | Planned goods movement date \(Edm.DateTime\) | +| ↳ `OverallSDProcessStatus` | string | Overall SD process \(delivery\) status | +| ↳ `OverallGoodsMovementStatus` | string | Overall goods movement status | +| ↳ `DocumentDate` | string | Document date \(Edm.DateTime\) | +| ↳ `CreationDate` | string | Creation date \(Edm.DateTime\) | +| ↳ `LastChangeDate` | string | Last change date \(Edm.DateTime\) | +| ↳ `__next` | string | OData skiptoken URL for next page | +| ↳ `__count` | string | Total count when $inlinecount=allpages is used | ### `sap_s4hana_get_inbound_delivery` @@ -608,10 +917,10 @@ Retrieve a single inbound delivery by DeliveryDocument key from SAP S/4HANA Clou | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -627,7 +936,24 @@ Retrieve a single inbound delivery by DeliveryDocument key from SAP S/4HANA Clou | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_InbDeliveryHeader entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_InbDeliveryHeader entity | +| ↳ `DeliveryDocument` | string | Inbound delivery number | +| ↳ `DeliveryDocumentType` | string | Delivery document type | +| ↳ `SDDocumentCategory` | string | SD document category \(e.g., 7 = inbound delivery\) | +| ↳ `ReceivingPlant` | string | Receiving plant | +| ↳ `Supplier` | string | Supplier business partner | +| ↳ `ShipToParty` | string | Ship-to business partner | +| ↳ `DeliveryDate` | string | Delivery date \(Edm.DateTime\) | +| ↳ `ActualGoodsMovementDate` | string | Actual goods movement \(receipt\) date \(Edm.DateTime\) | +| ↳ `PlannedGoodsMovementDate` | string | Planned goods movement date \(Edm.DateTime\) | +| ↳ `OverallSDProcessStatus` | string | Overall SD process \(delivery\) status | +| ↳ `OverallGoodsMovementStatus` | string | Overall goods movement status | +| ↳ `DocumentDate` | string | Document date \(Edm.DateTime\) | +| ↳ `CreationDate` | string | Creation date \(Edm.DateTime\) | +| ↳ `LastChangeDate` | string | Last change date \(Edm.DateTime\) | +| ↳ `to_DeliveryDocumentItem` | json | Delivery items \(when $expand=to_DeliveryDocumentItem\) | +| ↳ `to_DeliveryDocumentPartner` | json | Delivery partners \(when $expand=to_DeliveryDocumentPartner\) | ### `sap_s4hana_list_billing_documents` @@ -637,10 +963,10 @@ List billing documents (customer invoices) from SAP S/4HANA Cloud (API_BILLING_D | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -652,14 +978,47 @@ List billing documents (customer invoices) from SAP S/4HANA Cloud (API_BILLING_D | `skip` | number | No | Number of results to skip \($skip\) | | `orderBy` | string | No | OData $orderby expression | | `select` | string | No | Comma-separated fields to return \($select\) | -| `expand` | string | No | Comma-separated navigation properties to expand \(e.g., "to_Item,to_Partner"\) | +| `expand` | string | No | Comma-separated navigation properties to expand \(e.g., "to_Item,to_Partner,to_PricingElement"\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_BillingDocument entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_BillingDocument entities | +| ↳ `BillingDocument` | string | Billing document number | +| ↳ `SDDocumentCategory` | string | SD document category | +| ↳ `BillingDocumentCategory` | string | Billing document category | +| ↳ `BillingDocumentType` | string | Billing document type \(e.g., F2\) | +| ↳ `BillingDocumentDate` | string | Billing document date \(OData /Date\(ms\)/\) | +| ↳ `BillingDocumentIsCancelled` | boolean | Whether the billing document is cancelled | +| ↳ `CancelledBillingDocument` | string | Cancelled billing document number | +| ↳ `TotalNetAmount` | string | Total net amount \(Edm.Decimal as string\) | +| ↳ `TaxAmount` | string | Tax amount \(Edm.Decimal as string\) | +| ↳ `TotalGrossAmount` | string | Total gross amount \(Edm.Decimal as string\) | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `PayerParty` | string | Payer party | +| ↳ `SalesOrganization` | string | Sales organization | +| ↳ `DistributionChannel` | string | Distribution channel | +| ↳ `Division` | string | Division | +| ↳ `CompanyCode` | string | Company code | +| ↳ `FiscalYear` | string | Fiscal year | +| ↳ `OverallBillingStatus` | string | Overall billing status | +| ↳ `AccountingPostingStatus` | string | Accounting posting status | +| ↳ `AccountingTransferStatus` | string | Accounting transfer status | +| ↳ `InvoiceClearingStatus` | string | Invoice clearing status | +| ↳ `AccountingDocument` | string | Linked accounting document | +| ↳ `CustomerPaymentTerms` | string | Customer payment terms | +| ↳ `PaymentMethod` | string | Payment method | +| ↳ `DocumentReferenceID` | string | Document reference ID | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDate` | string | Last change date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDateTime` | string | Last change date-time \(Edm.DateTimeOffset\) | +| ↳ `__next` | string | OData skiptoken URL for next page | +| ↳ `__count` | string | Total count when $inlinecount=allpages is used | ### `sap_s4hana_get_billing_document` @@ -669,10 +1028,10 @@ Retrieve a single billing document (customer invoice) by BillingDocument key fro | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -681,14 +1040,47 @@ Retrieve a single billing document (customer invoice) by BillingDocument key fro | `password` | string | No | Password for HTTP Basic auth | | `billingDocument` | string | Yes | BillingDocument key \(string, up to 10 characters\) | | `select` | string | No | Comma-separated fields to return \($select\) | -| `expand` | string | No | Comma-separated navigation properties to expand \(e.g., "to_Item,to_Partner"\) | +| `expand` | string | No | Comma-separated navigation properties to expand \(e.g., "to_Item,to_Partner,to_PricingElement"\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_BillingDocument entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_BillingDocument entity | +| ↳ `BillingDocument` | string | Billing document number | +| ↳ `SDDocumentCategory` | string | SD document category | +| ↳ `BillingDocumentCategory` | string | Billing document category | +| ↳ `BillingDocumentType` | string | Billing document type | +| ↳ `BillingDocumentDate` | string | Billing document date \(OData /Date\(ms\)/\) | +| ↳ `BillingDocumentIsCancelled` | boolean | Whether the billing document is cancelled | +| ↳ `CancelledBillingDocument` | string | Cancelled billing document number | +| ↳ `TotalNetAmount` | string | Total net amount \(Edm.Decimal as string\) | +| ↳ `TaxAmount` | string | Tax amount \(Edm.Decimal as string\) | +| ↳ `TotalGrossAmount` | string | Total gross amount \(Edm.Decimal as string\) | +| ↳ `TransactionCurrency` | string | Document currency | +| ↳ `SoldToParty` | string | Sold-to business partner | +| ↳ `PayerParty` | string | Payer party | +| ↳ `SalesOrganization` | string | Sales organization | +| ↳ `DistributionChannel` | string | Distribution channel | +| ↳ `Division` | string | Division | +| ↳ `CompanyCode` | string | Company code | +| ↳ `FiscalYear` | string | Fiscal year | +| ↳ `OverallBillingStatus` | string | Overall billing status | +| ↳ `AccountingPostingStatus` | string | Accounting posting status | +| ↳ `AccountingTransferStatus` | string | Accounting transfer status | +| ↳ `InvoiceClearingStatus` | string | Invoice clearing status | +| ↳ `AccountingDocument` | string | Linked accounting document | +| ↳ `CustomerPaymentTerms` | string | Customer payment terms | +| ↳ `PaymentMethod` | string | Payment method | +| ↳ `DocumentReferenceID` | string | Document reference ID | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDate` | string | Last change date \(OData /Date\(ms\)/\) | +| ↳ `LastChangeDateTime` | string | Last change date-time \(Edm.DateTimeOffset\) | +| ↳ `to_Item` | json | Billing document items \(when $expand=to_Item\) | +| ↳ `to_Partner` | json | Billing document partners \(when $expand=to_Partner\) | +| ↳ `to_PricingElement` | json | Billing document pricing elements \(when $expand=to_PricingElement\) | ### `sap_s4hana_list_products` @@ -698,10 +1090,10 @@ List products (materials) from SAP S/4HANA Cloud (API_PRODUCT_SRV, A_Product) wi | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -720,7 +1112,30 @@ List products (materials) from SAP S/4HANA Cloud (API_PRODUCT_SRV, A_Product) wi | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_Product entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_Product entities | +| ↳ `Product` | string | Product \(material\) number | +| ↳ `ProductType` | string | Product type \(e.g., FERT, HAWA\) | +| ↳ `ProductGroup` | string | Material group | +| ↳ `BaseUnit` | string | Base unit of measure | +| ↳ `Brand` | string | Brand | +| ↳ `Division` | string | Division | +| ↳ `GrossWeight` | string | Gross weight | +| ↳ `NetWeight` | string | Net weight | +| ↳ `WeightUnit` | string | Weight unit of measure | +| ↳ `CrossPlantStatus` | string | Cross-plant material status | +| ↳ `IsMarkedForDeletion` | boolean | Deletion flag | +| ↳ `ProductStandardID` | string | Standard product ID \(e.g., GTIN\) | +| ↳ `ItemCategoryGroup` | string | Item category group | +| ↳ `ProductOldID` | string | Legacy/old product ID | +| ↳ `CreatedByUser` | string | User who created the product | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `LastChangedByUser` | string | User who last changed the product | +| ↳ `LastChangeDate` | string | Last change date | +| ↳ `LastChangeDateTime` | string | Last change timestamp \(Edm.DateTimeOffset\) | +| ↳ `__next` | string | OData skiptoken URL for next page | +| ↳ `__count` | string | Total count when $inlinecount=allpages is used | ### `sap_s4hana_get_product` @@ -730,10 +1145,10 @@ Retrieve a single product (material) by Product key from SAP S/4HANA Cloud (API_ | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -749,20 +1164,43 @@ Retrieve a single product (material) by Product key from SAP S/4HANA Cloud (API_ | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_Product entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_Product entity | +| ↳ `Product` | string | Product \(material\) number | +| ↳ `ProductType` | string | Product type \(e.g., FERT, HAWA\) | +| ↳ `ProductGroup` | string | Material group | +| ↳ `BaseUnit` | string | Base unit of measure | +| ↳ `Brand` | string | Brand | +| ↳ `Division` | string | Division | +| ↳ `GrossWeight` | string | Gross weight | +| ↳ `NetWeight` | string | Net weight | +| ↳ `WeightUnit` | string | Weight unit of measure | +| ↳ `CrossPlantStatus` | string | Cross-plant material status | +| ↳ `IsMarkedForDeletion` | boolean | Deletion flag | +| ↳ `ProductStandardID` | string | Standard product ID \(e.g., GTIN\) | +| ↳ `ItemCategoryGroup` | string | Item category group | +| ↳ `ProductOldID` | string | Legacy/old product ID | +| ↳ `CreatedByUser` | string | User who created the product | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `LastChangedByUser` | string | User who last changed the product | +| ↳ `LastChangeDate` | string | Last change date | +| ↳ `LastChangeDateTime` | string | Last change timestamp \(Edm.DateTimeOffset\) | +| ↳ `to_Description` | json | Product descriptions \(when $expand=to_Description\) | +| ↳ `to_Plant` | json | Plant-level data \(when $expand=to_Plant\) | +| ↳ `to_ProductSales` | json | Sales data \(when $expand=to_ProductSales\) | ### `sap_s4hana_update_product` -Update fields on an A_Product entity in SAP S/4HANA Cloud (API_PRODUCT_SRV). PATCH only sends the fields you provide; existing values are preserved. Flat scalar header fields only — deep/multi-entity updates across navigation properties are not supported by API_PRODUCT_SRV PATCH/PUT (see SAP KBA 2833338); update child entities (plant, valuation, sales data, etc.) via their own endpoints. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET. +Update fields on an A_Product entity in SAP S/4HANA Cloud (API_PRODUCT_SRV). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. Flat scalar header fields only — deep/multi-entity updates across navigation properties are not supported by API_PRODUCT_SRV MERGE/PUT (see SAP KBA 2833338); update child entities (plant, valuation, sales data, etc.) via their own endpoints. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -778,7 +1216,14 @@ Update fields on an A_Product entity in SAP S/4HANA Cloud (API_PRODUCT_SRV). PAT | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on 204 success, or updated A_Product entity if SAP returns one | +| `data` | json | Null on 204 success, or OData v2 envelope with the updated A_Product entity at output.data.d | +| ↳ `d` | json | Updated A_Product entity \(only present if SAP returns a body\) | +| ↳ `Product` | string | Product \(material\) number | +| ↳ `ProductType` | string | Product type | +| ↳ `ProductGroup` | string | Material group | +| ↳ `BaseUnit` | string | Base unit of measure | +| ↳ `IsMarkedForDeletion` | boolean | Deletion flag | +| ↳ `LastChangeDate` | string | Last change date | ### `sap_s4hana_list_material_stock` @@ -788,10 +1233,10 @@ List material stock quantities from SAP S/4HANA Cloud (API_MATERIAL_STOCK_SRV, A | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -810,7 +1255,20 @@ List material stock quantities from SAP S/4HANA Cloud (API_MATERIAL_STOCK_SRV, A | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_MatlStkInAcctMod stock entries | +| `data` | json | OData payload containing the array of A_MatlStkInAcctMod stock entries | +| ↳ `Material` | string | Material number | +| ↳ `Plant` | string | Plant identifier | +| ↳ `StorageLocation` | string | Storage location identifier | +| ↳ `Batch` | string | Batch identifier | +| ↳ `Supplier` | string | Supplier business partner key | +| ↳ `Customer` | string | Customer business partner key | +| ↳ `WBSElementInternalID` | string | WBS element internal ID | +| ↳ `SDDocument` | string | SD document number | +| ↳ `SDDocumentItem` | string | SD document item | +| ↳ `InventorySpecialStockType` | string | Special stock type indicator | +| ↳ `InventoryStockType` | string | Stock type \(e.g., 01 unrestricted-use, 02 quality inspection, 03 blocked, 04 restricted-use\) | +| ↳ `MatlWrhsStkQtyInMatlBaseUnit` | string | Material warehouse stock quantity in material base unit \(Edm.Decimal serialized as string\) | +| ↳ `MaterialBaseUnit` | string | Material base unit of measure | ### `sap_s4hana_list_material_documents` @@ -820,10 +1278,10 @@ List material document headers (goods movements) from SAP S/4HANA Cloud (API_MAT | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -842,7 +1300,22 @@ List material document headers (goods movements) from SAP S/4HANA Cloud (API_MAT | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_MaterialDocumentHeader entities | +| `data` | json | OData payload containing the array of A_MaterialDocumentHeader entities | +| ↳ `MaterialDocumentYear` | string | Material document year \(4-digit fiscal year\) | +| ↳ `MaterialDocument` | string | Material document number | +| ↳ `DocumentDate` | string | Document date \(OData /Date\(...\)/ string\) | +| ↳ `PostingDate` | string | Posting date \(OData /Date\(...\)/ string\) | +| ↳ `MaterialDocumentHeaderText` | string | Header text describing the material document | +| ↳ `ReferenceDocument` | string | Reference document number | +| ↳ `GoodsMovementCode` | string | Goods movement code \(e.g., 01 GR for PO, 03 GI to cost center\) | +| ↳ `InventoryTransactionType` | string | Inventory transaction type indicator | +| ↳ `CreatedByUser` | string | User who created the material document | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(...\)/ string\) | +| ↳ `CreationTime` | string | Creation time \(OData PT...S string\) | +| ↳ `VersionForPrintingSlip` | string | Version for printing the goods movement slip | +| ↳ `ManualPrintIsTriggered` | boolean | Indicates whether manual print was triggered for this document | +| ↳ `CtrlPostgForExtWhseMgmtSyst` | string | Control posting for external warehouse management system | +| ↳ `to_MaterialDocumentItem` | json | Material document items \(only present when $expand=to_MaterialDocumentItem is supplied\) | ### `sap_s4hana_get_material_document` @@ -852,10 +1325,10 @@ Retrieve a single material document header by composite key (MaterialDocument + | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -872,7 +1345,22 @@ Retrieve a single material document header by composite key (MaterialDocument + | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_MaterialDocumentHeader entity | +| `data` | json | OData payload containing the A_MaterialDocumentHeader entity \(and optionally to_MaterialDocumentItem when expanded\) | +| ↳ `MaterialDocumentYear` | string | Material document year \(4-digit fiscal year\) | +| ↳ `MaterialDocument` | string | Material document number | +| ↳ `DocumentDate` | string | Document date \(OData /Date\(...\)/ string\) | +| ↳ `PostingDate` | string | Posting date \(OData /Date\(...\)/ string\) | +| ↳ `MaterialDocumentHeaderText` | string | Header text describing the material document | +| ↳ `ReferenceDocument` | string | Reference document number | +| ↳ `GoodsMovementCode` | string | Goods movement code \(e.g., 01 GR for PO, 03 GI to cost center\) | +| ↳ `InventoryTransactionType` | string | Inventory transaction type indicator | +| ↳ `CreatedByUser` | string | User who created the material document | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(...\)/ string\) | +| ↳ `CreationTime` | string | Creation time \(OData PT...S string\) | +| ↳ `VersionForPrintingSlip` | string | Version for printing the goods movement slip | +| ↳ `ManualPrintIsTriggered` | boolean | Indicates whether manual print was triggered for this document | +| ↳ `CtrlPostgForExtWhseMgmtSyst` | string | Control posting for external warehouse management system | +| ↳ `to_MaterialDocumentItem` | json | Material document items \(only present when $expand=to_MaterialDocumentItem is supplied\) | ### `sap_s4hana_list_purchase_requisitions` @@ -882,10 +1370,10 @@ List purchase requisitions from SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -904,7 +1392,14 @@ List purchase requisitions from SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_PurchaseRequisitionHeader entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_PurchaseRequisitionHeader entities | +| ↳ `PurchaseRequisition` | string | Purchase requisition number | +| ↳ `PurchaseRequisitionType` | string | Purchase requisition document type \(e.g., NB\) | +| ↳ `PurReqnDescription` | string | Purchase requisition description | +| ↳ `SourceDetermination` | string | Source-of-supply determination flag | +| ↳ `__next` | string | OData skiptoken URL for next page | ### `sap_s4hana_get_purchase_requisition` @@ -914,10 +1409,10 @@ Retrieve a single purchase requisition by PurchaseRequisition key from SAP S/4HA | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -933,7 +1428,13 @@ Retrieve a single purchase requisition by PurchaseRequisition key from SAP S/4HA | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_PurchaseRequisitionHeader entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_PurchaseRequisitionHeader entity | +| ↳ `PurchaseRequisition` | string | Purchase requisition number | +| ↳ `PurchaseRequisitionType` | string | PR document type \(e.g., NB\) | +| ↳ `PurReqnDescription` | string | Purchase requisition description | +| ↳ `SourceDetermination` | string | Source-of-supply determination flag | +| ↳ `to_PurchaseReqnItem` | json | Expanded PR items \(when $expand=to_PurchaseReqnItem\) | ### `sap_s4hana_create_purchase_requisition` @@ -943,10 +1444,10 @@ Create a purchase requisition in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -955,27 +1456,33 @@ Create a purchase requisition in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV, | `password` | string | No | Password for HTTP Basic auth | | `purchaseRequisitionType` | string | Yes | PurchaseRequisitionType \(e.g., "NB" Standard PR\) | | `items` | json | Yes | to_PurchaseReqnItem deep-insert array \(e.g., \[\{"PurchaseRequisitionItem":"10","Material":"TG11","RequestedQuantity":"5","Plant":"1010","BaseUnit":"PC","DeliveryDate":"/Date\(1735689600000\)/"\}\]\) | -| `body` | json | No | Additional A_PurchaseRequisitionHeader fields merged into the create payload \(e.g., \{"PurchaseRequisitionDescription":"Office supplies"\}\) | +| `body` | json | No | Additional A_PurchaseRequisitionHeader fields merged into the create payload \(e.g., \{"PurReqnDescription":"Office supplies"\}\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Created A_PurchaseRequisitionHeader entity | +| `data` | json | OData v2 response envelope; created entity at output.data.d | +| ↳ `d` | json | Created A_PurchaseRequisitionHeader entity | +| ↳ `PurchaseRequisition` | string | Auto-assigned purchase requisition number | +| ↳ `PurchaseRequisitionType` | string | PR document type \(e.g., NB\) | +| ↳ `PurReqnDescription` | string | Purchase requisition description | +| ↳ `SourceDetermination` | string | Source-of-supply determination flag | +| ↳ `to_PurchaseReqnItem` | json | Created PR items returned in deep insert | ### `sap_s4hana_update_purchase_requisition` -Update fields on an A_PurchaseRequisitionHeader entity in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV; deprecated since S/4HANA 2402, successor is API_PURCHASEREQUISITION_2 OData v4). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. +Update fields on an A_PurchaseRequisitionHeader entity in SAP S/4HANA Cloud (API_PURCHASEREQ_PROCESS_SRV; deprecated since S/4HANA 2402, successor is API_PURCHASEREQUISITION_2 OData v4). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. Header-only — deep updates across navigations are not supported (SAP KBA 2833338); use the A_PurchaseReqnItem entity directly to modify items. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -991,7 +1498,12 @@ Update fields on an A_PurchaseRequisitionHeader entity in SAP S/4HANA Cloud (API | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on 204 success, or updated A_PurchaseRequisitionHeader entity if SAP returns one | +| `data` | json | Null on 204 success, or OData v2 envelope with updated A_PurchaseRequisitionHeader at output.data.d | +| ↳ `d` | json | Updated A_PurchaseRequisitionHeader entity \(if returned\) | +| ↳ `PurchaseRequisition` | string | Purchase requisition number | +| ↳ `PurchaseRequisitionType` | string | PR document type | +| ↳ `PurReqnDescription` | string | Purchase requisition description | +| ↳ `SourceDetermination` | string | Source-of-supply determination flag | ### `sap_s4hana_list_purchase_orders` @@ -1001,10 +1513,10 @@ List purchase orders from SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_Pu | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -1023,7 +1535,22 @@ List purchase orders from SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_Pu | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_PurchaseOrder entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_PurchaseOrder entities | +| ↳ `PurchaseOrder` | string | Purchase order number | +| ↳ `PurchaseOrderType` | string | PO document type \(e.g., NB\) | +| ↳ `CompanyCode` | string | Company code | +| ↳ `PurchasingOrganization` | string | Purchasing organization | +| ↳ `PurchasingGroup` | string | Purchasing group | +| ↳ `Supplier` | string | Supplier business partner key | +| ↳ `DocumentCurrency` | string | Document currency | +| ↳ `NetAmount` | string | Net amount of the purchase order | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `CreatedByUser` | string | User who created the PO | +| ↳ `PurchaseOrderDate` | string | Purchase order date | +| ↳ `__next` | string | OData skiptoken URL for next page | +| ↳ `__count` | string | Total count when $inlinecount=allpages is used | ### `sap_s4hana_get_purchase_order` @@ -1033,10 +1560,10 @@ Retrieve a single purchase order by PurchaseOrder key from SAP S/4HANA Cloud (AP | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -1052,7 +1579,25 @@ Retrieve a single purchase order by PurchaseOrder key from SAP S/4HANA Cloud (AP | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_PurchaseOrder entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_PurchaseOrder entity | +| ↳ `PurchaseOrder` | string | Purchase order number | +| ↳ `PurchaseOrderType` | string | PO document type | +| ↳ `CompanyCode` | string | Company code | +| ↳ `PurchasingOrganization` | string | Purchasing organization | +| ↳ `PurchasingGroup` | string | Purchasing group | +| ↳ `Supplier` | string | Supplier business partner key | +| ↳ `DocumentCurrency` | string | Document currency | +| ↳ `NetAmount` | string | Net amount of the purchase order | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `CreatedByUser` | string | User who created the PO | +| ↳ `PurchaseOrderDate` | string | Purchase order date | +| ↳ `ValidityStartDate` | string | Validity start date | +| ↳ `ValidityEndDate` | string | Validity end date | +| ↳ `IncotermsClassification` | string | Incoterms classification \(e.g., FOB\) | +| ↳ `PaymentTerms` | string | Payment terms key | +| ↳ `LastChangeDateTime` | string | Last change timestamp \(OData /Date\(ms\)/\) | +| ↳ `to_PurchaseOrderItem` | json | Expanded PO items \(when $expand=to_PurchaseOrderItem\) | ### `sap_s4hana_create_purchase_order` @@ -1062,10 +1607,10 @@ Create a purchase order in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_P | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -1084,20 +1629,31 @@ Create a purchase order in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV, A_P | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Created A_PurchaseOrder entity | +| `data` | json | OData v2 response envelope; created entity at output.data.d | +| ↳ `d` | json | Created A_PurchaseOrder entity | +| ↳ `PurchaseOrder` | string | Auto-assigned purchase order number | +| ↳ `PurchaseOrderType` | string | PO document type | +| ↳ `CompanyCode` | string | Company code | +| ↳ `PurchasingOrganization` | string | Purchasing organization | +| ↳ `PurchasingGroup` | string | Purchasing group | +| ↳ `Supplier` | string | Supplier business partner key | +| ↳ `DocumentCurrency` | string | Document currency | +| ↳ `NetAmount` | string | Net amount of the purchase order | +| ↳ `CreationDate` | string | Creation date \(OData /Date\(ms\)/\) | +| ↳ `to_PurchaseOrderItem` | json | Created PO items returned in deep insert | ### `sap_s4hana_update_purchase_order` -Update fields on an A_PurchaseOrder entity in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV). PATCH only sends the fields you provide; existing values are preserved. If-Match defaults to a wildcard (unconditional) — for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. +Update fields on an A_PurchaseOrder header in SAP S/4HANA Cloud (API_PURCHASEORDER_PROCESS_SRV). Uses HTTP MERGE (OData v2 partial update) — only the fields you provide are written; existing values are preserved. Header-only — line-item changes are not supported via deep update on the header (SAP KBA 2833338); use the A_PurchaseOrderItem entity directly to modify items. If-Match defaults to a wildcard - for safe concurrent updates pass the ETag from a prior GET to avoid lost updates. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -1113,7 +1669,16 @@ Update fields on an A_PurchaseOrder entity in SAP S/4HANA Cloud (API_PURCHASEORD | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP \(204 on success\) | -| `data` | json | Null on 204 success, or updated A_PurchaseOrder entity if SAP returns one | +| `data` | json | Null on 204 success, or OData v2 envelope with updated A_PurchaseOrder at output.data.d | +| ↳ `d` | json | Updated A_PurchaseOrder entity \(if returned\) | +| ↳ `PurchaseOrder` | string | Purchase order number | +| ↳ `PurchaseOrderType` | string | PO document type | +| ↳ `CompanyCode` | string | Company code | +| ↳ `PurchasingGroup` | string | Purchasing group | +| ↳ `Supplier` | string | Supplier key | +| ↳ `NetAmount` | string | Net amount | +| ↳ `DocumentCurrency` | string | Document currency | +| ↳ `LastChangeDateTime` | string | Last change timestamp | ### `sap_s4hana_list_supplier_invoices` @@ -1123,10 +1688,10 @@ List supplier invoices from SAP S/4HANA Cloud (API_SUPPLIERINVOICE_PROCESS_SRV, | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -1145,7 +1710,26 @@ List supplier invoices from SAP S/4HANA Cloud (API_SUPPLIERINVOICE_PROCESS_SRV, | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | Array of A_SupplierInvoice entities | +| `data` | json | OData v2 response envelope; collection at output.data.d.results | +| ↳ `d` | json | OData v2 envelope | +| ↳ `results` | array | A_SupplierInvoice entities | +| ↳ `SupplierInvoice` | string | Supplier invoice number | +| ↳ `FiscalYear` | string | Fiscal year | +| ↳ `CompanyCode` | string | Company code | +| ↳ `DocumentDate` | string | Invoice document date | +| ↳ `PostingDate` | string | Posting date | +| ↳ `InvoicingParty` | string | Invoicing party \(supplier key\) | +| ↳ `InvoiceGrossAmount` | string | Gross invoice amount | +| ↳ `DocumentCurrency` | string | Document currency | +| ↳ `AccountingDocumentType` | string | Accounting document type | +| ↳ `PaymentTerms` | string | Payment terms key | +| ↳ `DueCalculationBaseDate` | string | Baseline date for due-date calculation | +| ↳ `SupplierInvoiceIDByInvcgParty` | string | Reference number used by the invoicing party | +| ↳ `PaymentMethod` | string | Payment method | +| ↳ `TaxIsCalculatedAutomatically` | boolean | Whether tax is calculated automatically | +| ↳ `ManualCashDiscount` | string | Manually entered cash discount amount | +| ↳ `BusinessPlace` | string | Business place \(jurisdiction code\) | +| ↳ `__next` | string | OData skiptoken URL for next page | ### `sap_s4hana_get_supplier_invoice` @@ -1155,10 +1739,10 @@ Retrieve a single supplier invoice by composite key (SupplierInvoice + FiscalYea | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | @@ -1175,20 +1759,37 @@ Retrieve a single supplier invoice by composite key (SupplierInvoice + FiscalYea | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | number | HTTP status code returned by SAP | -| `data` | json | A_SupplierInvoice entity | +| `data` | json | OData v2 response envelope; entity at output.data.d | +| ↳ `d` | json | A_SupplierInvoice entity | +| ↳ `SupplierInvoice` | string | Supplier invoice number | +| ↳ `FiscalYear` | string | Fiscal year | +| ↳ `CompanyCode` | string | Company code | +| ↳ `DocumentDate` | string | Invoice document date | +| ↳ `PostingDate` | string | Posting date | +| ↳ `InvoicingParty` | string | Invoicing party \(supplier key\) | +| ↳ `InvoiceGrossAmount` | string | Gross invoice amount | +| ↳ `DocumentCurrency` | string | Document currency | +| ↳ `AccountingDocumentType` | string | Accounting document type | +| ↳ `PaymentTerms` | string | Payment terms key | +| ↳ `DueCalculationBaseDate` | string | Baseline date for due-date calculation | +| ↳ `SupplierInvoiceIDByInvcgParty` | string | Reference number used by the invoicing party | +| ↳ `PaymentMethod` | string | Payment method | +| ↳ `TaxIsCalculatedAutomatically` | boolean | Whether tax is calculated automatically | +| ↳ `ManualCashDiscount` | string | Manually entered cash discount amount | +| ↳ `BusinessPlace` | string | Business place \(jurisdiction code\) | ### `sap_s4hana_odata_query` -Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping. +Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping. For write operations (POST/PUT/PATCH/MERGE/DELETE), pass an If-Match ETag obtained from a prior GET to avoid lost updates; misuse will mutate production data. #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `subdomain` | string | Yes | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | -| `region` | string | Yes | BTP region \(e.g. eu10, us10\) | -| `clientId` | string | Yes | OAuth client ID from the S/4HANA Communication Arrangement | -| `clientSecret` | string | Yes | OAuth client secret from the S/4HANA Communication Arrangement | +| `subdomain` | string | No | SAP BTP subaccount subdomain \(technical name of your subaccount, not the S/4HANA host\) | +| `region` | string | No | BTP region \(e.g. eu10, us10\) | +| `clientId` | string | No | OAuth client ID from the S/4HANA Communication Arrangement | +| `clientSecret` | string | No | OAuth client secret from the S/4HANA Communication Arrangement | | `deploymentType` | string | No | Deployment type: cloud_public \(default\), cloud_private, or on_premise | | `authType` | string | No | Authentication type: oauth_client_credentials \(default\) or basic | | `baseUrl` | string | No | Base URL of the S/4HANA host \(Cloud Private / On-Premise\) | diff --git a/apps/docs/content/docs/en/tools/sharepoint.mdx b/apps/docs/content/docs/en/tools/sharepoint.mdx index cf83987bd40..05a425ba35e 100644 --- a/apps/docs/content/docs/en/tools/sharepoint.mdx +++ b/apps/docs/content/docs/en/tools/sharepoint.mdx @@ -278,5 +278,15 @@ Upload files to a SharePoint document library | ↳ `createdDateTime` | string | When the file was created | | ↳ `lastModifiedDateTime` | string | When the file was last modified | | `fileCount` | number | Number of files uploaded | +| `skippedFiles` | array | Files that were skipped before upload | +| ↳ `name` | string | File name | +| ↳ `size` | number | File size in bytes | +| ↳ `limit` | number | Upload size limit in bytes | +| ↳ `reason` | string | Reason the file was skipped | +| `skippedCount` | number | Number of files skipped | +| `errors` | array | Per-file upload errors | +| ↳ `name` | string | File name | +| ↳ `error` | string | Error message | +| ↳ `status` | number | HTTP status from Microsoft Graph | diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index d1dc93e0a96..e19d3d267b1 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -155,6 +155,7 @@ import { RootlyIcon, S3Icon, SalesforceIcon, + SapConcurIcon, SapS4HanaIcon, SESIcon, SearchIcon, @@ -354,6 +355,7 @@ export const blockTypeToIconMap: Record = { rootly: RootlyIcon, s3: S3Icon, salesforce: SalesforceIcon, + sap_concur: SapConcurIcon, sap_s4hana: SapS4HanaIcon, search: SearchIcon, secrets_manager: SecretsManagerIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 113c411c190..9a1513dc6f8 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -11482,6 +11482,305 @@ "integrationTypes": ["crm", "customer-support", "sales"], "tags": ["sales-engagement", "customer-support"] }, + { + "type": "sap_concur", + "slug": "sap-concur", + "name": "SAP Concur", + "description": "Manage expense reports, travel requests, cash advances, and more in SAP Concur", + "longDescription": "Connect SAP Concur via OAuth 2.0. Manage expense reports and line items, allocations, attendees, comments, exceptions, quick expenses, receipts, travel requests and expected expenses, cash advances, itineraries, user identities, custom lists, budgets, exchange rates, and purchase requests across every Concur datacenter.", + "bgColor": "#FFFFFF", + "iconName": "SapConcurIcon", + "docsUrl": "https://docs.sim.ai/tools/sap_concur", + "operations": [ + { + "name": "List Expense Reports", + "description": "List expense reports (GET /api/v3.0/expense/reports). Returns a v3 envelope 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, diff --git a/apps/sim/app/api/tools/sap_concur/proxy/route.ts b/apps/sim/app/api/tools/sap_concur/proxy/route.ts new file mode 100644 index 00000000000..802d83be266 --- /dev/null +++ b/apps/sim/app/api/tools/sap_concur/proxy/route.ts @@ -0,0 +1,133 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { getValidationErrorMessage, isZodError } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { + assertSafeExternalUrl, + extractSapConcurError, + fetchSapConcurAccessToken, + SAP_CONCUR_OUTBOUND_FETCH_TIMEOUT_MS, + type SapConcurProxyRequest, + SapConcurProxyRequestSchema, +} from '@/app/api/tools/sap_concur/shared' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SapConcurProxyAPI') + +type ProxyRequest = SapConcurProxyRequest + +function buildApiUrl(geolocation: string, req: ProxyRequest): string { + const base = geolocation.replace(/\/+$/, '') + const subPath = req.path.startsWith('/') ? req.path : `/${req.path}` + const url = `${base}${subPath}` + + if (!req.query || Object.keys(req.query).length === 0) { + return url + } + const search = new URLSearchParams() + for (const [key, value] of Object.entries(req.query)) { + if (value === undefined || value === null) continue + search.append(key, String(value)) + } + const queryString = search.toString() + if (!queryString) return url + return url.includes('?') ? `${url}&${queryString}` : `${url}?${queryString}` +} + +interface Invocation { + status: number + body: unknown + raw: string +} + +async function callConcur( + req: ProxyRequest, + accessToken: string, + geolocation: string +): Promise { + const url = assertSafeExternalUrl(buildApiUrl(geolocation, req), 'apiUrl').toString() + const hasBody = req.body !== undefined && req.body !== null + const headers: Record = { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + } + if (hasBody) headers['Content-Type'] = req.contentType ?? 'application/json' + if (req.companyUuid) headers['concur-correlationid'] = req.companyUuid + + const response = await secureFetchWithValidation( + url, + { + method: req.method, + headers, + body: hasBody + ? typeof req.body === 'string' + ? req.body + : JSON.stringify(req.body) + : undefined, + timeout: SAP_CONCUR_OUTBOUND_FETCH_TIMEOUT_MS, + }, + 'apiUrl' + ) + + const raw = await response.text() + let parsed: unknown = null + if (raw.length > 0) { + try { + parsed = JSON.parse(raw) + } catch { + parsed = raw + } + } + return { status: response.status, body: parsed, raw } +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Concur proxy request: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + // boundary-raw-json: internal proxy envelope validated by SapConcurProxyRequestSchema below; not a public boundary + const json = await request.json() + const proxyReq = SapConcurProxyRequestSchema.parse(json) + + const { accessToken, geolocation } = await fetchSapConcurAccessToken(proxyReq, requestId) + const invocation = await callConcur(proxyReq, accessToken, geolocation) + + if (invocation.status >= 200 && invocation.status < 300) { + const data = invocation.status === 204 ? null : invocation.body + return NextResponse.json({ success: true, output: { status: invocation.status, data } }) + } + + const message = extractSapConcurError(invocation.body, invocation.status) + logger.warn( + `[${requestId}] Concur API error (${invocation.status}) ${proxyReq.path}: ${message}` + ) + return NextResponse.json( + { success: false, error: message, status: invocation.status }, + { status: invocation.status } + ) + } catch (error) { + if (isZodError(error)) { + logger.warn(`[${requestId}] Validation error:`, error.issues) + return NextResponse.json( + { success: false, error: getValidationErrorMessage(error, 'Validation failed') }, + { status: 400 } + ) + } + logger.error(`[${requestId}] Unexpected Concur proxy error:`, error) + return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/sap_concur/shared.ts b/apps/sim/app/api/tools/sap_concur/shared.ts new file mode 100644 index 00000000000..e395b4123af --- /dev/null +++ b/apps/sim/app/api/tools/sap_concur/shared.ts @@ -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 + +export const SapConcurHttpMethod = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) + +export const SapConcurProxyPath = z + .string() + .min(1, 'path is required') + .refine( + (p) => + !p.split(/[/\\]/).some((seg) => seg === '..' || seg === '.') && + !p.includes('#') && + !/%(?:2[eEfF]|5[cC]|23)/.test(p), + { + message: + 'path must not contain ".." or "." segments, "#", or percent-encoded path/fragment characters', + } + ) + +export const SapConcurProxyRequestSchema = SapConcurAuthSchema.extend({ + path: SapConcurProxyPath, + method: SapConcurHttpMethod.default('GET'), + query: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(), + body: z.unknown().optional(), + contentType: z.string().optional(), +}).superRefine((req, ctx) => { + if (req.grantType === 'password') { + if (!req.username) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['username'], + message: 'username is required for password grant', + }) + } + if (!req.password) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['password'], + message: 'password is required for password grant', + }) + } + } +}) + +export type SapConcurProxyRequest = z.infer + +export const SapConcurUploadOperation = z.enum([ + 'upload_receipt_image', + 'create_quick_expense_with_image', +]) + +export const SapConcurUploadRequestSchema = SapConcurAuthSchema.extend({ + operation: SapConcurUploadOperation, + userId: z.string().min(1, 'userId is required'), + contextType: z.string().optional(), + receipt: FileInputSchema, + forwardId: z.string().max(40).optional(), + body: z.union([z.record(z.string(), z.unknown()), z.string()]).optional(), +}) + +export type SapConcurUploadRequest = z.infer + +const FORBIDDEN_HOSTS = new Set([ + 'localhost', + '0.0.0.0', + '127.0.0.1', + '169.254.169.254', + 'metadata.google.internal', + 'metadata', + '[::1]', + '[::]', + '[::ffff:127.0.0.1]', + '[fd00:ec2::254]', +]) + +function isPrivateIPv4(host: string): boolean { + const match = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) + if (!match) return false + const octets = match.slice(1, 5).map(Number) as [number, number, number, number] + if (octets.some((o) => o < 0 || o > 255)) return false + const [a, b] = octets + if (a === 10) return true + if (a === 172 && b >= 16 && b <= 31) return true + if (a === 192 && b === 168) return true + if (a === 127) return true + if (a === 169 && b === 254) return true + if (a === 0) return true + return false +} + +function isPrivateOrLoopbackIPv6(host: string): boolean { + const stripped = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host + const lower = stripped.toLowerCase() + if (lower === '::' || lower === '::1') return true + if (/^fc[0-9a-f]{2}:/.test(lower) || /^fd[0-9a-f]{2}:/.test(lower)) return true + if (lower.startsWith('fe80:')) return true + return false +} + +/** Validate a URL is https and not pointing to a private/loopback host. */ +export function assertSafeExternalUrl(rawUrl: string, label: string): URL { + let parsed: URL + try { + parsed = new URL(rawUrl) + } catch { + throw new Error(`${label} must be a valid URL`) + } + if (parsed.protocol !== 'https:') { + throw new Error(`${label} must use https://`) + } + const host = parsed.hostname.toLowerCase() + if (FORBIDDEN_HOSTS.has(host) || FORBIDDEN_HOSTS.has(`[${host}]`)) { + throw new Error(`${label} host is not allowed`) + } + if (isPrivateIPv4(host)) { + throw new Error(`${label} host is not allowed (private/loopback range)`) + } + if (isPrivateOrLoopbackIPv6(host)) { + throw new Error(`${label} host is not allowed (IPv6 private/loopback)`) + } + return parsed +} + +interface CachedToken { + accessToken: string + geolocation: string + expiresAt: number +} + +const TOKEN_CACHE = new Map() +const TOKEN_CACHE_MAX_ENTRIES = 500 +const TOKEN_SAFETY_WINDOW_MS = 60_000 +export const SAP_CONCUR_OUTBOUND_FETCH_TIMEOUT_MS = 30_000 + +function tokenCacheKey(req: SapConcurAuth): string { + const secretHash = createHash('sha256').update(req.clientSecret).digest('hex').slice(0, 16) + const userHash = req.username + ? createHash('sha256').update(req.username).digest('hex').slice(0, 12) + : '' + return `${req.datacenter}::${req.grantType}::${req.clientId}::${secretHash}::${userHash}` +} + +function rememberToken(key: string, token: CachedToken): void { + if (TOKEN_CACHE.has(key)) TOKEN_CACHE.delete(key) + TOKEN_CACHE.set(key, token) + while (TOKEN_CACHE.size > TOKEN_CACHE_MAX_ENTRIES) { + const oldestKey = TOKEN_CACHE.keys().next().value + if (oldestKey === undefined) break + TOKEN_CACHE.delete(oldestKey) + } +} + +function normalizeGeolocation(raw: string | undefined, fallback: string): string { + if (!raw) return `https://${fallback}` + const trimmed = raw.replace(/\/+$/, '') + if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) return trimmed + return `https://${trimmed}` +} + +/** + * Acquire a Concur access token, sharing a cache with the proxy route. + * Validates that the geolocation returned by Concur is a safe external URL. + */ +export async function fetchSapConcurAccessToken( + auth: SapConcurAuth, + requestId: string +): Promise<{ accessToken: string; geolocation: string }> { + if (auth.grantType === 'password') { + if (!auth.username) throw new Error('username is required for password grant') + if (!auth.password) throw new Error('password is required for password grant') + } + + const cacheKey = tokenCacheKey(auth) + const cached = TOKEN_CACHE.get(cacheKey) + if (cached && cached.expiresAt - TOKEN_SAFETY_WINDOW_MS > Date.now()) { + return { accessToken: cached.accessToken, geolocation: cached.geolocation } + } + + const tokenUrl = assertSafeExternalUrl( + `https://${auth.datacenter}/oauth2/v0/token`, + 'tokenUrl' + ).toString() + + const params = new URLSearchParams() + params.set('client_id', auth.clientId) + params.set('client_secret', auth.clientSecret) + params.set('grant_type', auth.grantType) + if (auth.grantType === 'password') { + params.set('username', auth.username ?? '') + params.set('password', auth.password ?? '') + if (auth.companyUuid) params.set('credtype', 'authtoken') + } + + const response = await secureFetchWithValidation( + tokenUrl, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }, + body: params.toString(), + timeout: SAP_CONCUR_OUTBOUND_FETCH_TIMEOUT_MS, + }, + 'tokenUrl' + ) + + if (!response.ok) { + const text = await response.text().catch(() => '') + logger.warn(`[${requestId}] Concur token fetch failed (${response.status}): ${text}`) + throw new Error(`Concur token request failed: HTTP ${response.status}`) + } + + const data = (await response.json()) as { + access_token?: string + expires_in?: number + geolocation?: string + } + + if (!data.access_token) { + throw new Error('Concur token response missing access_token') + } + + const geolocation = normalizeGeolocation(data.geolocation, auth.datacenter) + 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 + if (typeof obj.error === 'string' && obj.error.length > 0) { + const desc = typeof obj.error_description === 'string' ? `: ${obj.error_description}` : '' + return `${obj.error}${desc}` + } + if (typeof obj.message === 'string' && obj.message.length > 0) { + return obj.message + } + const errors = obj.errors + if (Array.isArray(errors) && errors.length > 0) { + return errors + .map((e) => { + if (e && typeof e === 'object') { + const eo = e as Record + const code = typeof eo.errorCode === 'string' ? `[${eo.errorCode}] ` : '' + const msg = typeof eo.errorMessage === 'string' ? eo.errorMessage : '' + return `${code}${msg}`.trim() + } + return String(e) + }) + .filter(Boolean) + .join('; ') + } + } + if (typeof body === 'string' && body.length > 0) return body + return `Concur request failed with HTTP ${status}` +} diff --git a/apps/sim/app/api/tools/sap_concur/upload/route.ts b/apps/sim/app/api/tools/sap_concur/upload/route.ts new file mode 100644 index 00000000000..81885d9a29f --- /dev/null +++ b/apps/sim/app/api/tools/sap_concur/upload/route.ts @@ -0,0 +1,279 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { getValidationErrorMessage, isZodError } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils' +import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { + assertSafeExternalUrl, + extractSapConcurError, + fetchSapConcurAccessToken, + SAP_CONCUR_OUTBOUND_FETCH_TIMEOUT_MS, + type SapConcurUploadRequest, + SapConcurUploadRequestSchema, +} from '@/app/api/tools/sap_concur/shared' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('SapConcurUploadAPI') + +type UploadRequest = SapConcurUploadRequest + +const RECEIPT_ALLOWED_MIME_TYPES = new Set([ + 'application/pdf', + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/gif', + 'image/tiff', +]) + +const QUICK_EXPENSE_ALLOWED_MIME_TYPES = new Set([ + 'application/pdf', + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/tiff', +]) + +const ALLOWED_MIME_TYPES = RECEIPT_ALLOWED_MIME_TYPES + +function inferMimeType(name: string, declared?: string): string { + if (declared && ALLOWED_MIME_TYPES.has(declared.toLowerCase())) { + return declared.toLowerCase() === 'image/jpg' ? 'image/jpeg' : declared.toLowerCase() + } + const lower = name.toLowerCase() + if (lower.endsWith('.pdf')) return 'application/pdf' + if (lower.endsWith('.png')) return 'image/png' + if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg' + if (lower.endsWith('.gif')) return 'image/gif' + if (lower.endsWith('.tif') || lower.endsWith('.tiff')) return 'image/tiff' + return 'application/octet-stream' +} + +function stringifyMaybeJson(value: unknown): string { + if (typeof value === 'string') return value + return JSON.stringify(value ?? {}) +} + +interface UploadInvocation { + status: number + body: unknown +} + +async function postMultipart( + url: string, + accessToken: string, + formData: FormData, + companyUuid: string | undefined, + extraHeaders?: Record +): Promise { + const headers: Record = { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', + ...(extraHeaders ?? {}), + } + if (companyUuid) headers['concur-correlationid'] = companyUuid + + // Serialize FormData (with auto-generated multipart boundary) to a Buffer so we can + // route through secureFetchWithValidation (which doesn't support FormData bodies directly). + const serialized = new Request('http://localhost/internal-multipart-serializer', { + method: 'POST', + body: formData, + }) + const contentType = serialized.headers.get('content-type') + if (contentType) headers['Content-Type'] = contentType + const bodyBuffer = Buffer.from(await serialized.arrayBuffer()) + + const response = await secureFetchWithValidation( + url, + { + method: 'POST', + headers, + body: bodyBuffer, + timeout: SAP_CONCUR_OUTBOUND_FETCH_TIMEOUT_MS, + }, + 'apiUrl' + ) + + const raw = await response.text() + let parsed: unknown = null + if (raw.length > 0) { + try { + parsed = JSON.parse(raw) + } catch { + parsed = raw + } + } + // Surface Location/Link headers for receipt endpoints that return 202 with no body. + if ( + parsed === null || + (typeof parsed === 'object' && parsed !== null && Object.keys(parsed).length === 0) + ) { + const location = response.headers.get('Location') + const link = response.headers.get('Link') + if (location || link) { + parsed = { location, link } + } + } + return { status: response.status, body: parsed } +} + +async function handleUploadReceiptImage( + req: UploadRequest, + fileBuffer: Buffer, + fileName: string, + mimeType: string, + accessToken: string, + geolocation: string +): Promise { + const url = assertSafeExternalUrl( + `${geolocation.replace(/\/+$/, '')}/receipts/v4/users/${encodeURIComponent(req.userId)}/image-only-receipts`, + 'apiUrl' + ).toString() + + const formData = new FormData() + formData.append('image', new Blob([new Uint8Array(fileBuffer)], { type: mimeType }), fileName) + + const extraHeaders: Record | undefined = req.forwardId + ? { 'concur-forwardid': req.forwardId } + : undefined + + return postMultipart(url, accessToken, formData, req.companyUuid, extraHeaders) +} + +async function handleCreateQuickExpenseWithImage( + req: UploadRequest, + fileBuffer: Buffer, + fileName: string, + mimeType: string, + accessToken: string, + geolocation: string +): Promise { + const contextType = req.contextType?.trim() || 'TRAVELER' + const url = assertSafeExternalUrl( + `${geolocation.replace(/\/+$/, '')}/quickexpense/v4/users/${encodeURIComponent( + req.userId + )}/context/${encodeURIComponent(contextType)}/quickexpenses/image`, + 'apiUrl' + ).toString() + + const quickExpenseRequest = stringifyMaybeJson(req.body ?? {}) + + const formData = new FormData() + formData.append('quickExpenseRequest', quickExpenseRequest) + formData.append( + 'fileContent', + new Blob([new Uint8Array(fileBuffer)], { type: mimeType }), + fileName + ) + + return postMultipart(url, accessToken, formData, req.companyUuid) +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Concur upload request: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + // boundary-raw-json: internal upload envelope validated by SapConcurUploadRequestSchema below; not a public boundary + const json = await request.json() + const uploadReq = SapConcurUploadRequestSchema.parse(json) + + const userFiles = processFilesToUserFiles( + [uploadReq.receipt as RawFileInput], + requestId, + logger + ) + if (userFiles.length === 0) { + return NextResponse.json( + { success: false, error: 'Invalid receipt file input' }, + { status: 400 } + ) + } + const userFile = userFiles[0] + const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) + const fileName = userFile.name + const mimeType = inferMimeType(fileName, userFile.type) + + const allowedForOperation = + uploadReq.operation === 'create_quick_expense_with_image' + ? QUICK_EXPENSE_ALLOWED_MIME_TYPES + : RECEIPT_ALLOWED_MIME_TYPES + if (!allowedForOperation.has(mimeType)) { + const allowedLabel = + uploadReq.operation === 'create_quick_expense_with_image' + ? 'pdf, png, jpeg, tiff' + : 'pdf, png, jpeg, gif, tiff' + return NextResponse.json( + { + success: false, + error: `Unsupported receipt mime type: ${mimeType}. Allowed: ${allowedLabel}`, + }, + { status: 400 } + ) + } + + const { accessToken, geolocation } = await fetchSapConcurAccessToken(uploadReq, requestId) + + let invocation: UploadInvocation + if (uploadReq.operation === 'upload_receipt_image') { + invocation = await handleUploadReceiptImage( + uploadReq, + fileBuffer, + fileName, + mimeType, + accessToken, + geolocation + ) + } else { + invocation = await handleCreateQuickExpenseWithImage( + uploadReq, + fileBuffer, + fileName, + mimeType, + accessToken, + geolocation + ) + } + + if (invocation.status >= 200 && invocation.status < 300) { + const data = invocation.status === 204 ? null : invocation.body + logger.info( + `[${requestId}] Concur ${uploadReq.operation} succeeded: HTTP ${invocation.status}` + ) + return NextResponse.json({ success: true, output: { status: invocation.status, data } }) + } + + const message = extractSapConcurError(invocation.body, invocation.status) + logger.warn( + `[${requestId}] Concur upload error (${invocation.status}) ${uploadReq.operation}: ${message}` + ) + return NextResponse.json( + { success: false, error: message, status: invocation.status }, + { status: invocation.status } + ) + } catch (error) { + if (isZodError(error)) { + logger.warn(`[${requestId}] Validation error:`, error.issues) + return NextResponse.json( + { success: false, error: getValidationErrorMessage(error, 'Validation failed') }, + { status: 400 } + ) + } + logger.error(`[${requestId}] Unexpected Concur upload error:`, error) + return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/sap_s4hana/proxy/route.ts b/apps/sim/app/api/tools/sap_s4hana/proxy/route.ts index d3414a48af1..bee4a8b84aa 100644 --- a/apps/sim/app/api/tools/sap_s4hana/proxy/route.ts +++ b/apps/sim/app/api/tools/sap_s4hana/proxy/route.ts @@ -9,6 +9,10 @@ import { } from '@/lib/api/contracts/tools/sap' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + type SecureFetchResponse, + secureFetchWithValidation, +} from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -65,16 +69,20 @@ async function fetchAccessToken(req: ProxyRequest, requestId: string): Promise '') @@ -104,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 { const url = buildOdataUrl(req, '/$metadata') - const response = await fetch(url, { - method: 'GET', - headers: { - Authorization: buildAuthHeader(req, accessToken), - Accept: 'application/xml', - 'X-CSRF-Token': 'Fetch', + const response = await secureFetchWithValidation( + url, + { + method: 'GET', + headers: { + Authorization: buildAuthHeader(req, accessToken), + Accept: 'application/xml', + 'X-CSRF-Token': 'Fetch', + }, + timeout: OUTBOUND_FETCH_TIMEOUT_MS, }, - signal: AbortSignal.timeout(OUTBOUND_FETCH_TIMEOUT_MS), - }) + 'baseUrl' + ) if (!response.ok) { const text = await response.text().catch(() => '') @@ -146,7 +155,7 @@ async function fetchCsrf( } const token = response.headers.get('x-csrf-token') - const cookie = joinSetCookies(response.headers) + const cookie = joinSetCookies(response) if (!token) return null return { token, cookie } } @@ -217,12 +226,16 @@ async function callOdata( if (csrf.cookie) headers.Cookie = csrf.cookie } - const response = await fetch(url, { - method: req.method, - headers, - body: hasBody ? JSON.stringify(req.body) : undefined, - signal: AbortSignal.timeout(OUTBOUND_FETCH_TIMEOUT_MS), - }) + const response = await secureFetchWithValidation( + url, + { + method: req.method, + headers, + body: hasBody ? JSON.stringify(req.body) : undefined, + timeout: OUTBOUND_FETCH_TIMEOUT_MS, + }, + 'baseUrl' + ) const raw = await response.text() let parsed: unknown = null diff --git a/apps/sim/blocks/blocks/sap_concur.ts b/apps/sim/blocks/blocks/sap_concur.ts new file mode 100644 index 00000000000..ac5ede1dcc9 --- /dev/null +++ b/apps/sim/blocks/blocks/sap_concur.ts @@ -0,0 +1,1901 @@ +import { SapConcurIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import { normalizeFileInput } from '@/blocks/utils' +import type { SapConcurProxyResponse, UserFileLike } from '@/tools/sap_concur/types' + +const toBool = (v: unknown): boolean | undefined => { + if (v === undefined || v === null || v === '') return undefined + if (typeof v === 'boolean') return v + if (typeof v === 'string') return v.toLowerCase() === 'true' + return Boolean(v) +} + +const REPORT_USER_OPS = [ + 'sap_concur_list_expense_reports', + 'sap_concur_get_expense_report', + 'sap_concur_create_expense_report', + 'sap_concur_update_expense_report', + 'sap_concur_submit_expense_report', + 'sap_concur_recall_expense_report', + 'sap_concur_list_expenses', + 'sap_concur_get_expense', + 'sap_concur_get_itemizations', + 'sap_concur_list_allocations', + 'sap_concur_get_allocation', + 'sap_concur_update_allocation', + 'sap_concur_list_attendee_associations', + 'sap_concur_associate_attendees', + 'sap_concur_remove_all_attendees', + 'sap_concur_list_report_comments', + 'sap_concur_create_report_comment', + 'sap_concur_list_exceptions', + 'sap_concur_create_quick_expense', + 'sap_concur_create_quick_expense_with_image', + 'sap_concur_list_receipts', + 'sap_concur_list_reports_to_approve', + 'sap_concur_upload_receipt_image', +] + +const REPORT_GET_CONTEXT_TYPE_OPS = ['sap_concur_get_expense_report'] + +const EXPENSE_READ_CONTEXT_TYPE_OPS = [ + 'sap_concur_list_expense_reports', + 'sap_concur_list_expenses', + 'sap_concur_get_expense', + 'sap_concur_get_itemizations', + 'sap_concur_list_exceptions', +] + +const QUICK_EXPENSE_CONTEXT_TYPE_OPS = [ + 'sap_concur_create_quick_expense', + 'sap_concur_create_quick_expense_with_image', +] + +const MANAGER_ONLY_CONTEXT_TYPE_OPS = ['sap_concur_list_reports_to_approve'] + +const ATTENDEE_CONTEXT_TYPE_OPS = [ + 'sap_concur_list_attendee_associations', + 'sap_concur_associate_attendees', + 'sap_concur_remove_all_attendees', + 'sap_concur_create_report_comment', + 'sap_concur_list_report_comments', +] + +const ALLOCATION_CONTEXT_TYPE_OPS = [ + 'sap_concur_list_allocations', + 'sap_concur_get_allocation', + 'sap_concur_update_allocation', + 'sap_concur_recall_expense_report', + 'sap_concur_create_expense_report', + 'sap_concur_update_expense_report', +] + +const REPORT_ID_OPS = [ + 'sap_concur_get_expense_report', + 'sap_concur_update_expense_report', + 'sap_concur_delete_expense_report', + 'sap_concur_submit_expense_report', + 'sap_concur_recall_expense_report', + 'sap_concur_approve_expense_report', + 'sap_concur_send_back_expense_report', + 'sap_concur_list_expenses', + 'sap_concur_get_expense', + 'sap_concur_get_itemizations', + 'sap_concur_update_expense', + 'sap_concur_delete_expense', + 'sap_concur_list_allocations', + 'sap_concur_get_allocation', + 'sap_concur_update_allocation', + 'sap_concur_list_attendee_associations', + 'sap_concur_associate_attendees', + 'sap_concur_remove_all_attendees', + 'sap_concur_list_report_comments', + 'sap_concur_create_report_comment', + 'sap_concur_list_exceptions', +] + +const EXPENSE_ID_OPS = [ + 'sap_concur_get_expense', + 'sap_concur_get_itemizations', + 'sap_concur_update_expense', + 'sap_concur_delete_expense', + 'sap_concur_list_allocations', + 'sap_concur_list_attendee_associations', + 'sap_concur_associate_attendees', + 'sap_concur_remove_all_attendees', +] + +const REQUEST_UUID_OPS = [ + 'sap_concur_get_travel_request', + 'sap_concur_update_travel_request', + 'sap_concur_delete_travel_request', + 'sap_concur_move_travel_request', + 'sap_concur_list_travel_request_comments', + 'sap_concur_create_expected_expense', + 'sap_concur_list_expected_expenses', +] + +const RECEIPT_UPLOAD_OPS = [ + 'sap_concur_upload_receipt_image', + 'sap_concur_create_quick_expense_with_image', +] + +const LIST_ITEM_ID_OPS = [ + 'sap_concur_get_list_item', + 'sap_concur_update_list_item', + 'sap_concur_delete_list_item', +] + +const BODY_OPS = [ + 'sap_concur_create_expense_report', + 'sap_concur_update_expense_report', + 'sap_concur_submit_expense_report', + 'sap_concur_recall_expense_report', + 'sap_concur_approve_expense_report', + 'sap_concur_send_back_expense_report', + 'sap_concur_update_expense', + 'sap_concur_update_allocation', + 'sap_concur_associate_attendees', + 'sap_concur_create_list_item', + 'sap_concur_create_quick_expense', + 'sap_concur_create_quick_expense_with_image', + 'sap_concur_create_travel_request', + 'sap_concur_update_list_item', + 'sap_concur_update_travel_request', + 'sap_concur_move_travel_request', + 'sap_concur_create_expected_expense', + 'sap_concur_update_expected_expense', + 'sap_concur_create_cash_advance', + 'sap_concur_issue_cash_advance', + 'sap_concur_create_user', + 'sap_concur_update_user', + 'sap_concur_search_users', + 'sap_concur_create_purchase_request', + 'sap_concur_upload_exchange_rates', +] + +export const SapConcurBlock: BlockConfig = { + type: 'sap_concur', + name: 'SAP Concur', + description: 'Manage expense reports, travel requests, cash advances, and more in SAP Concur', + authMode: AuthMode.ApiKey, + longDescription: + 'Connect SAP Concur via OAuth 2.0. Manage expense reports and line items, allocations, attendees, comments, exceptions, quick expenses, receipts, travel requests and expected expenses, cash advances, itineraries, user identities, custom lists, budgets, exchange rates, and purchase requests across every Concur datacenter.', + docsLink: 'https://docs.sim.ai/tools/sap_concur', + category: 'tools', + integrationType: IntegrationType.Other, + tags: ['automation'], + bgColor: '#FFFFFF', + icon: SapConcurIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'List Expense Reports', id: 'sap_concur_list_expense_reports' }, + { label: 'Get Expense Report', id: 'sap_concur_get_expense_report' }, + { label: 'Create Expense Report', id: 'sap_concur_create_expense_report' }, + { label: 'Update Expense Report', id: 'sap_concur_update_expense_report' }, + { label: 'Delete Expense Report', id: 'sap_concur_delete_expense_report' }, + { label: 'Submit Expense Report', id: 'sap_concur_submit_expense_report' }, + { label: 'Recall Expense Report', id: 'sap_concur_recall_expense_report' }, + { label: 'Approve Expense Report', id: 'sap_concur_approve_expense_report' }, + { label: 'Send Back Expense Report', id: 'sap_concur_send_back_expense_report' }, + { label: 'List Reports To Approve', id: 'sap_concur_list_reports_to_approve' }, + { label: 'List Expenses', id: 'sap_concur_list_expenses' }, + { label: 'Get Expense', id: 'sap_concur_get_expense' }, + { label: 'Update Expense', id: 'sap_concur_update_expense' }, + { label: 'Delete Expense', id: 'sap_concur_delete_expense' }, + { label: 'Get Itemizations', id: 'sap_concur_get_itemizations' }, + { label: 'List Allocations', id: 'sap_concur_list_allocations' }, + { label: 'Get Allocation', id: 'sap_concur_get_allocation' }, + { label: 'Update Allocation', id: 'sap_concur_update_allocation' }, + { label: 'List Attendee Associations', id: 'sap_concur_list_attendee_associations' }, + { label: 'Associate Attendees', id: 'sap_concur_associate_attendees' }, + { label: 'Remove All Attendees', id: 'sap_concur_remove_all_attendees' }, + { label: 'List Report Comments', id: 'sap_concur_list_report_comments' }, + { label: 'Create Report Comment', id: 'sap_concur_create_report_comment' }, + { label: 'List Exceptions', id: 'sap_concur_list_exceptions' }, + { label: 'Create Quick Expense', id: 'sap_concur_create_quick_expense' }, + { + label: 'Create Quick Expense (With Image)', + id: 'sap_concur_create_quick_expense_with_image', + }, + { label: 'List Receipts', id: 'sap_concur_list_receipts' }, + { label: 'Get Receipt', id: 'sap_concur_get_receipt' }, + { label: 'Get Receipt Status', id: 'sap_concur_get_receipt_status' }, + { label: 'Upload Receipt Image', id: 'sap_concur_upload_receipt_image' }, + { label: 'List Travel Requests', id: 'sap_concur_list_travel_requests' }, + { label: 'Get Travel Request', id: 'sap_concur_get_travel_request' }, + { label: 'Create Travel Request', id: 'sap_concur_create_travel_request' }, + { label: 'Update Travel Request', id: 'sap_concur_update_travel_request' }, + { label: 'Delete Travel Request', id: 'sap_concur_delete_travel_request' }, + { label: 'Move Travel Request (Workflow Action)', id: 'sap_concur_move_travel_request' }, + { + label: 'List Travel Request Comments', + id: 'sap_concur_list_travel_request_comments', + }, + { + label: 'Get Request Cash Advance', + id: 'sap_concur_get_request_cash_advance', + }, + { label: 'Create Expected Expense', id: 'sap_concur_create_expected_expense' }, + { label: 'List Expected Expenses', id: 'sap_concur_list_expected_expenses' }, + { label: 'Get Expected Expense', id: 'sap_concur_get_expected_expense' }, + { label: 'Update Expected Expense', id: 'sap_concur_update_expected_expense' }, + { label: 'Delete Expected Expense', id: 'sap_concur_delete_expected_expense' }, + { label: 'Create Cash Advance', id: 'sap_concur_create_cash_advance' }, + { label: 'Get Cash Advance', id: 'sap_concur_get_cash_advance' }, + { label: 'Issue Cash Advance', id: 'sap_concur_issue_cash_advance' }, + { label: 'List Itineraries (Trips)', id: 'sap_concur_list_itineraries' }, + { label: 'Get Itinerary (Trip)', id: 'sap_concur_get_itinerary' }, + { label: 'List Users', id: 'sap_concur_list_users' }, + { label: 'Get User', id: 'sap_concur_get_user' }, + { label: 'Create User', id: 'sap_concur_create_user' }, + { label: 'Update User (PATCH)', id: 'sap_concur_update_user' }, + { label: 'Delete User', id: 'sap_concur_delete_user' }, + { label: 'Search Users', id: 'sap_concur_search_users' }, + { label: 'List Lists', id: 'sap_concur_list_lists' }, + { label: 'Get List', id: 'sap_concur_get_list' }, + { label: 'List List Items', id: 'sap_concur_list_list_items' }, + { label: 'Get List Item', id: 'sap_concur_get_list_item' }, + { label: 'Create List Item', id: 'sap_concur_create_list_item' }, + { label: 'Update List Item', id: 'sap_concur_update_list_item' }, + { label: 'Delete List Item', id: 'sap_concur_delete_list_item' }, + { label: 'List Budgets', id: 'sap_concur_list_budgets' }, + { label: 'Get Budget', id: 'sap_concur_get_budget' }, + { label: 'List Budget Categories', id: 'sap_concur_list_budget_categories' }, + { label: 'Upload Exchange Rates', id: 'sap_concur_upload_exchange_rates' }, + { label: 'Create Purchase Request', id: 'sap_concur_create_purchase_request' }, + { label: 'Get Purchase Request', id: 'sap_concur_get_purchase_request' }, + { label: 'Get Travel Profile', id: 'sap_concur_get_travel_profile' }, + { + label: 'List Travel Profiles Summary', + id: 'sap_concur_list_travel_profiles_summary', + }, + { label: 'Search Locations', id: 'sap_concur_search_locations' }, + ], + value: () => 'sap_concur_list_expense_reports', + required: true, + }, + + // Auth fields + { + id: 'datacenter', + title: 'Datacenter', + type: 'dropdown', + options: [ + { label: 'US (us.api.concursolutions.com)', id: 'us.api.concursolutions.com' }, + { label: 'US 2 (us2.api.concursolutions.com)', id: 'us2.api.concursolutions.com' }, + { label: 'EU (eu.api.concursolutions.com)', id: 'eu.api.concursolutions.com' }, + { label: 'EU 2 (eu2.api.concursolutions.com)', id: 'eu2.api.concursolutions.com' }, + { label: 'EMEA (emea.api.concursolutions.com)', id: 'emea.api.concursolutions.com' }, + { label: 'CN (cn.api.concursolutions.com)', id: 'cn.api.concursolutions.com' }, + ], + value: () => 'us.api.concursolutions.com', + required: true, + }, + { + id: 'grantType', + title: 'OAuth Grant Type', + type: 'dropdown', + options: [ + { label: 'Client Credentials', id: 'client_credentials' }, + { label: 'Password', id: 'password' }, + ], + value: () => 'client_credentials', + }, + { + id: 'clientId', + title: 'OAuth Client ID', + type: 'short-input', + placeholder: 'Concur OAuth client ID', + password: true, + required: true, + }, + { + id: 'clientSecret', + title: 'OAuth Client Secret', + type: 'short-input', + placeholder: 'Concur OAuth client secret', + password: true, + required: true, + }, + { + id: 'username', + title: 'Username', + type: 'short-input', + placeholder: 'Username (password grant only)', + condition: { field: 'grantType', value: 'password' }, + required: { field: 'grantType', value: 'password' }, + }, + { + id: 'password', + title: 'Password', + type: 'short-input', + placeholder: 'Password (password grant only)', + password: true, + condition: { field: 'grantType', value: 'password' }, + required: { field: 'grantType', value: 'password' }, + }, + { + id: 'companyUuid', + title: 'Company UUID', + type: 'short-input', + placeholder: 'Multi-company access token UUID (optional)', + mode: 'advanced', + }, + + // Shared user/context fields for expense report ops + { + id: 'userId', + title: 'User ID', + type: 'short-input', + placeholder: 'Concur user UUID', + condition: { field: 'operation', value: REPORT_USER_OPS }, + required: { field: 'operation', value: REPORT_USER_OPS }, + }, + { + id: 'contextType', + title: 'Context Type', + type: 'dropdown', + options: [ + { label: 'TRAVELER', id: 'TRAVELER' }, + { label: 'MANAGER', id: 'MANAGER' }, + { label: 'PROCESSOR', id: 'PROCESSOR' }, + { label: 'PROXY', id: 'PROXY' }, + ], + value: () => 'TRAVELER', + condition: { field: 'operation', value: REPORT_GET_CONTEXT_TYPE_OPS }, + required: { field: 'operation', value: REPORT_GET_CONTEXT_TYPE_OPS }, + }, + { + id: 'contextType', + title: 'Context Type', + type: 'dropdown', + options: [ + { label: 'TRAVELER', id: 'TRAVELER' }, + { label: 'MANAGER', id: 'MANAGER' }, + { label: 'PROXY', id: 'PROXY' }, + ], + value: () => 'TRAVELER', + condition: { field: 'operation', value: EXPENSE_READ_CONTEXT_TYPE_OPS }, + required: { field: 'operation', value: EXPENSE_READ_CONTEXT_TYPE_OPS }, + }, + { + id: 'contextType', + title: 'Context Type', + type: 'dropdown', + options: [{ label: 'TRAVELER', id: 'TRAVELER' }], + value: () => 'TRAVELER', + condition: { field: 'operation', value: QUICK_EXPENSE_CONTEXT_TYPE_OPS }, + required: { field: 'operation', value: QUICK_EXPENSE_CONTEXT_TYPE_OPS }, + }, + { + id: 'contextType', + title: 'Context Type', + type: 'dropdown', + options: [ + { label: 'TRAVELER', id: 'TRAVELER' }, + { label: 'PROXY', id: 'PROXY' }, + ], + value: () => 'TRAVELER', + condition: { field: 'operation', value: ALLOCATION_CONTEXT_TYPE_OPS }, + required: { field: 'operation', value: ALLOCATION_CONTEXT_TYPE_OPS }, + }, + { + id: 'contextType', + title: 'Context Type', + type: 'dropdown', + options: [ + { label: 'TRAVELER', id: 'TRAVELER' }, + { label: 'PROXY', id: 'PROXY' }, + ], + value: () => 'TRAVELER', + condition: { field: 'operation', value: ATTENDEE_CONTEXT_TYPE_OPS }, + required: { field: 'operation', value: ATTENDEE_CONTEXT_TYPE_OPS }, + }, + { + id: 'contextType', + title: 'Context Type', + type: 'dropdown', + options: [{ label: 'MANAGER', id: 'MANAGER' }], + value: () => 'MANAGER', + condition: { field: 'operation', value: MANAGER_ONLY_CONTEXT_TYPE_OPS }, + required: { field: 'operation', value: MANAGER_ONLY_CONTEXT_TYPE_OPS }, + }, + + // Report ID + { + id: 'reportId', + title: 'Report ID', + type: 'short-input', + placeholder: 'Report ID', + condition: { field: 'operation', value: REPORT_ID_OPS }, + required: { field: 'operation', value: REPORT_ID_OPS }, + }, + { + id: 'expenseId', + title: 'Expense ID', + type: 'short-input', + placeholder: 'Expense entry ID', + condition: { field: 'operation', value: EXPENSE_ID_OPS }, + required: { field: 'operation', value: EXPENSE_ID_OPS }, + }, + { + id: 'allocationId', + title: 'Allocation ID', + type: 'short-input', + placeholder: 'Allocation ID', + condition: { + field: 'operation', + value: ['sap_concur_get_allocation', 'sap_concur_update_allocation'], + }, + required: { + field: 'operation', + value: ['sap_concur_get_allocation', 'sap_concur_update_allocation'], + }, + }, + { + id: 'expenseReportUser', + title: 'User', + type: 'short-input', + placeholder: 'Login ID or user identifier', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'approvalStatusCode', + title: 'Approval Status Code', + type: 'short-input', + placeholder: 'A_NOTF, A_PEND, A_APPR...', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'paymentStatusCode', + title: 'Payment Status Code', + type: 'short-input', + placeholder: 'P_NOTP, P_PAID...', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'currencyCode', + title: 'Currency Code', + type: 'short-input', + placeholder: 'USD, EUR...', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'approverLoginID', + title: 'Approver Login ID', + type: 'short-input', + placeholder: 'approver@example.com', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'submitDateAfter', + title: 'Submit Date After', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'submitDateBefore', + title: 'Submit Date Before', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'paidDateAfter', + title: 'Paid Date After', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'paidDateBefore', + title: 'Paid Date Before', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'modifiedDateAfter', + title: 'Modified Date After', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'modifiedDateBefore', + title: 'Modified Date Before', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'createDateAfter', + title: 'Create Date After', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'createDateBefore', + title: 'Create Date Before', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_expense_reports' }, + mode: 'advanced', + }, + { + id: 'comment', + title: 'Comment', + type: 'long-input', + placeholder: 'Comment text', + condition: { field: 'operation', value: 'sap_concur_create_report_comment' }, + required: { field: 'operation', value: 'sap_concur_create_report_comment' }, + }, + { + id: 'includeAllComments', + title: 'Include All Comments', + type: 'switch', + condition: { field: 'operation', value: 'sap_concur_list_report_comments' }, + mode: 'advanced', + }, + + // Receipt + { + id: 'receiptId', + title: 'Receipt ID', + type: 'short-input', + placeholder: 'Receipt ID', + condition: { + field: 'operation', + value: ['sap_concur_get_receipt', 'sap_concur_get_receipt_status'], + }, + required: { + field: 'operation', + value: ['sap_concur_get_receipt', 'sap_concur_get_receipt_status'], + }, + }, + + // Travel Requests + { + id: 'requestUuid', + title: 'Travel Request UUID', + type: 'short-input', + placeholder: 'Travel request UUID', + condition: { field: 'operation', value: REQUEST_UUID_OPS }, + required: { field: 'operation', value: REQUEST_UUID_OPS }, + }, + { + id: 'view', + title: 'View', + type: 'short-input', + placeholder: 'ALL, ACTIVE, PENDING, TOAPPROVE', + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + }, + { + id: 'travelRequestApprovedBefore', + title: 'Approved Before', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + mode: 'advanced', + }, + { + id: 'travelRequestApprovedAfter', + title: 'Approved After', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + mode: 'advanced', + }, + { + id: 'travelRequestModifiedBefore', + title: 'Modified Before', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + mode: 'advanced', + }, + { + id: 'travelRequestModifiedAfter', + title: 'Modified After', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + mode: 'advanced', + }, + { + id: 'travelRequestSortField', + title: 'Sort Field', + type: 'short-input', + placeholder: 'startDate', + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + mode: 'advanced', + }, + { + id: 'travelRequestSortOrder', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Ascending', id: 'asc' }, + { label: 'Descending', id: 'desc' }, + ], + condition: { field: 'operation', value: 'sap_concur_list_travel_requests' }, + mode: 'advanced', + }, + { + id: 'travelRequestUserId', + title: 'User ID', + type: 'short-input', + placeholder: 'Concur user UUID (optional impersonation)', + condition: { + field: 'operation', + value: [ + 'sap_concur_list_travel_requests', + 'sap_concur_get_travel_request', + 'sap_concur_create_travel_request', + 'sap_concur_delete_travel_request', + 'sap_concur_move_travel_request', + ], + }, + mode: 'advanced', + }, + { + id: 'action', + title: 'Workflow Action', + type: 'dropdown', + options: [ + { label: 'Submit', id: 'submit' }, + { label: 'Recall', id: 'recall' }, + { label: 'Cancel', id: 'cancel' }, + { label: 'Approve', id: 'approve' }, + { label: 'Send Back', id: 'sendback' }, + { label: 'Close', id: 'close' }, + { label: 'Reopen', id: 'reopen' }, + ], + value: () => 'submit', + condition: { field: 'operation', value: 'sap_concur_move_travel_request' }, + required: { field: 'operation', value: 'sap_concur_move_travel_request' }, + }, + + // Expected Expenses + { + id: 'expectedExpenseUserId', + title: 'User ID', + type: 'short-input', + placeholder: 'Concur user UUID (optional impersonation)', + condition: { + field: 'operation', + value: [ + 'sap_concur_list_expected_expenses', + 'sap_concur_create_expected_expense', + 'sap_concur_get_expected_expense', + 'sap_concur_update_expected_expense', + 'sap_concur_delete_expected_expense', + ], + }, + mode: 'advanced', + }, + { + id: 'expenseUuid', + title: 'Expected Expense UUID', + type: 'short-input', + placeholder: 'Expected expense UUID', + condition: { + field: 'operation', + value: [ + 'sap_concur_get_expected_expense', + 'sap_concur_update_expected_expense', + 'sap_concur_delete_expected_expense', + ], + }, + required: { + field: 'operation', + value: [ + 'sap_concur_get_expected_expense', + 'sap_concur_update_expected_expense', + 'sap_concur_delete_expected_expense', + ], + }, + }, + + // Cash advances + { + id: 'cashAdvanceUuid', + title: 'Cash Advance UUID', + type: 'short-input', + placeholder: 'Cash advance UUID', + condition: { field: 'operation', value: 'sap_concur_get_request_cash_advance' }, + required: { field: 'operation', value: 'sap_concur_get_request_cash_advance' }, + }, + { + id: 'cashAdvanceId', + title: 'Cash Advance ID', + type: 'short-input', + placeholder: 'Cash advance ID', + condition: { + field: 'operation', + value: ['sap_concur_get_cash_advance', 'sap_concur_issue_cash_advance'], + }, + required: { + field: 'operation', + value: ['sap_concur_get_cash_advance', 'sap_concur_issue_cash_advance'], + }, + }, + + // Itineraries + { + id: 'tripId', + title: 'Trip ID', + type: 'short-input', + placeholder: 'Trip ID', + condition: { field: 'operation', value: 'sap_concur_get_itinerary' }, + required: { field: 'operation', value: 'sap_concur_get_itinerary' }, + }, + { + id: 'useridType', + title: 'User ID Type', + type: 'dropdown', + options: [ + { label: 'Default', id: '' }, + { label: 'login', id: 'login' }, + { label: 'xmlsyncid', id: 'xmlsyncid' }, + { label: 'uuid', id: 'uuid' }, + ], + value: () => '', + condition: { + field: 'operation', + value: [ + 'sap_concur_get_itinerary', + 'sap_concur_list_itineraries', + 'sap_concur_get_travel_profile', + ], + }, + mode: 'advanced', + }, + { + id: 'useridValue', + title: 'User ID Value', + type: 'short-input', + placeholder: 'User identifier value', + condition: { + field: 'operation', + value: [ + 'sap_concur_get_itinerary', + 'sap_concur_list_itineraries', + 'sap_concur_get_travel_profile', + ], + }, + mode: 'advanced', + }, + { + id: 'systemFormat', + title: 'System Format', + type: 'short-input', + placeholder: 'GDS', + condition: { field: 'operation', value: 'sap_concur_get_itinerary' }, + mode: 'advanced', + }, + { + id: 'startDate', + title: 'Start Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + }, + { + id: 'endDate', + title: 'End Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + }, + { + id: 'bookingType', + title: 'Booking Type', + type: 'short-input', + placeholder: 'air, car, hotel, rail', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + mode: 'advanced', + }, + { + id: 'itineraryItemsPerPage', + title: 'Items Per Page', + type: 'short-input', + placeholder: '25', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + }, + { + id: 'itineraryPage', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + }, + { + id: 'includeMetadata', + title: 'Include Metadata', + type: 'switch', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + mode: 'advanced', + }, + { + id: 'includeCanceledTrips', + title: 'Include Canceled Trips', + type: 'switch', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + mode: 'advanced', + }, + { + id: 'createdAfterDate', + title: 'Created After Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + mode: 'advanced', + }, + { + id: 'createdBeforeDate', + title: 'Created Before Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + mode: 'advanced', + }, + { + id: 'itineraryLastModifiedDate', + title: 'Last Modified Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'operation', value: 'sap_concur_list_itineraries' }, + mode: 'advanced', + }, + + // Users + { + id: 'userUuid', + title: 'User UUID', + type: 'short-input', + placeholder: 'User UUID', + condition: { + field: 'operation', + value: ['sap_concur_get_user', 'sap_concur_update_user', 'sap_concur_delete_user'], + }, + required: { + field: 'operation', + value: ['sap_concur_get_user', 'sap_concur_update_user', 'sap_concur_delete_user'], + }, + }, + { + id: 'count', + title: 'Count', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: 'sap_concur_list_users' }, + }, + { + id: 'usersCursor', + title: 'Cursor', + type: 'short-input', + placeholder: 'Pagination cursor from previous response', + condition: { field: 'operation', value: 'sap_concur_list_users' }, + }, + { + id: 'attributes', + title: 'Attributes', + type: 'short-input', + placeholder: 'id,active,emails', + condition: { + field: 'operation', + value: ['sap_concur_list_users', 'sap_concur_get_user'], + }, + }, + { + id: 'excludedAttributes', + title: 'Excluded Attributes', + type: 'short-input', + placeholder: 'name,emails', + condition: { + field: 'operation', + value: ['sap_concur_list_users', 'sap_concur_get_user'], + }, + mode: 'advanced', + }, + + // Lists + { + id: 'listId', + title: 'List ID', + type: 'short-input', + placeholder: 'List ID', + condition: { + field: 'operation', + value: ['sap_concur_get_list', 'sap_concur_list_list_items'], + }, + required: { + field: 'operation', + value: ['sap_concur_get_list', 'sap_concur_list_list_items'], + }, + }, + // Budgets + { + id: 'budgetId', + title: 'Budget Item Header ID', + type: 'short-input', + placeholder: 'Budget header syncguid', + condition: { field: 'operation', value: 'sap_concur_get_budget' }, + required: { field: 'operation', value: 'sap_concur_get_budget' }, + }, + { + id: 'adminView', + title: 'Admin View', + type: 'switch', + condition: { field: 'operation', value: 'sap_concur_list_budgets' }, + }, + { + id: 'responseSchema', + title: 'Response Schema', + type: 'dropdown', + options: [ + { label: 'Default', id: '' }, + { label: 'Compact', id: 'COMPACT' }, + ], + value: () => '', + condition: { field: 'operation', value: 'sap_concur_list_budgets' }, + }, + + // Purchase Requests + { + id: 'purchaseRequestId', + title: 'Purchase Request ID', + type: 'short-input', + placeholder: 'Purchase request ID', + condition: { field: 'operation', value: 'sap_concur_get_purchase_request' }, + required: { field: 'operation', value: 'sap_concur_get_purchase_request' }, + }, + + // Pagination (shared across many list ops) + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '25', + condition: { + field: 'operation', + value: ['sap_concur_list_expense_reports', 'sap_concur_list_travel_requests'], + }, + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: '0', + condition: { + field: 'operation', + value: ['sap_concur_list_budgets', 'sap_concur_list_expense_reports'], + }, + }, + { + id: 'page', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { + field: 'operation', + value: ['sap_concur_list_lists', 'sap_concur_list_list_items'], + }, + }, + { + id: 'sortBy', + title: 'Sort By', + type: 'short-input', + placeholder: 'name', + condition: { + field: 'operation', + value: ['sap_concur_list_lists', 'sap_concur_list_list_items'], + }, + mode: 'advanced', + }, + { + id: 'sortDirection', + title: 'Sort Direction', + type: 'dropdown', + options: [ + { label: 'Ascending', id: 'asc' }, + { label: 'Descending', id: 'desc' }, + ], + condition: { + field: 'operation', + value: ['sap_concur_list_lists', 'sap_concur_list_list_items'], + }, + mode: 'advanced', + }, + { + id: 'reportsToApproveSort', + title: 'Sort By', + type: 'short-input', + placeholder: 'reportDate', + condition: { field: 'operation', value: 'sap_concur_list_reports_to_approve' }, + mode: 'advanced', + }, + { + id: 'reportsToApproveOrder', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Ascending', id: 'asc' }, + { label: 'Descending', id: 'desc' }, + ], + condition: { field: 'operation', value: 'sap_concur_list_reports_to_approve' }, + mode: 'advanced', + }, + { + id: 'includeDelegateApprovals', + title: 'Include Delegate Approvals', + type: 'switch', + condition: { field: 'operation', value: 'sap_concur_list_reports_to_approve' }, + mode: 'advanced', + }, + { + id: 'start', + title: 'Start', + type: 'short-input', + placeholder: '0', + condition: { + field: 'operation', + value: ['sap_concur_list_travel_requests'], + }, + }, + + // List Item ID (for update/delete list item) + { + id: 'itemId', + title: 'List Item ID', + type: 'short-input', + placeholder: 'List item UUID', + condition: { field: 'operation', value: LIST_ITEM_ID_OPS }, + required: { field: 'operation', value: LIST_ITEM_ID_OPS }, + }, + + // Travel Profile fields + { + id: 'lastModifiedDate', + title: 'Last Modified Date', + type: 'short-input', + placeholder: '1900-01-01T00:00:00 (UTC datetime)', + condition: { + field: 'operation', + value: 'sap_concur_list_travel_profiles_summary', + }, + required: { + field: 'operation', + value: 'sap_concur_list_travel_profiles_summary', + }, + }, + { + id: 'travelProfilePage', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: 'sap_concur_list_travel_profiles_summary' }, + }, + { + id: 'itemsPerPage', + title: 'Items Per Page', + type: 'short-input', + placeholder: '200', + condition: { field: 'operation', value: 'sap_concur_list_travel_profiles_summary' }, + }, + { + id: 'travelConfigs', + title: 'Travel Config IDs', + type: 'short-input', + placeholder: 'Comma-separated config ids', + condition: { field: 'operation', value: 'sap_concur_list_travel_profiles_summary' }, + }, + + // Locations fields (v5) + { + id: 'searchText', + title: 'Search Text', + type: 'short-input', + placeholder: 'Free-text search (city, landmark, etc.)', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + }, + { + id: 'locCode', + title: 'Location Code', + type: 'short-input', + placeholder: 'IATA / city code (e.g., SEA)', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + }, + { + id: 'locationNameId', + title: 'Location Name ID', + type: 'short-input', + placeholder: 'Concur location name id', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + mode: 'advanced', + }, + { + id: 'locationNameKey', + title: 'Location Name Key', + type: 'short-input', + placeholder: 'Concur location name key', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + mode: 'advanced', + }, + { + id: 'countryCode', + title: 'Country Code (ISO 3166-1)', + type: 'short-input', + placeholder: 'US', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + }, + { + id: 'subdivisionCode', + title: 'Subdivision Code (ISO 3166-2)', + type: 'short-input', + placeholder: 'US-WA', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + }, + { + id: 'adminRegionId', + title: 'Administrative Region ID', + type: 'short-input', + placeholder: 'Concur admin region id', + condition: { field: 'operation', value: 'sap_concur_search_locations' }, + mode: 'advanced', + }, + + // Receipt Image (basic mode — file picker) + { + id: 'receiptFile', + title: 'Receipt Image', + type: 'file-upload', + canonicalParamId: 'receipt', + placeholder: 'Upload receipt image', + condition: { field: 'operation', value: RECEIPT_UPLOAD_OPS }, + mode: 'basic', + multiple: false, + required: true, + acceptedTypes: 'image/jpeg,image/png,image/gif,image/tiff,application/pdf', + }, + // Receipt Image (advanced mode — variable reference) + { + id: 'receiptFileRef', + title: 'Receipt Image', + type: 'short-input', + canonicalParamId: 'receipt', + placeholder: 'Reference file from previous block', + condition: { field: 'operation', value: RECEIPT_UPLOAD_OPS }, + mode: 'advanced', + required: true, + }, + { + id: 'forwardId', + title: 'Forward ID', + type: 'short-input', + placeholder: 'Optional dedup id (max 40 chars)', + condition: { field: 'operation', value: 'sap_concur_upload_receipt_image' }, + mode: 'advanced', + }, + + // Body (JSON payload) — shared across all create/update/action ops + { + id: 'body', + title: 'Request Body (JSON)', + type: 'long-input', + placeholder: '{ ... }', + condition: { field: 'operation', value: BODY_OPS }, + required: { + field: 'operation', + value: [ + 'sap_concur_create_expense_report', + 'sap_concur_update_expense_report', + 'sap_concur_approve_expense_report', + 'sap_concur_send_back_expense_report', + 'sap_concur_update_expense', + 'sap_concur_update_allocation', + 'sap_concur_associate_attendees', + 'sap_concur_create_quick_expense', + 'sap_concur_create_quick_expense_with_image', + 'sap_concur_create_travel_request', + 'sap_concur_update_travel_request', + 'sap_concur_create_expected_expense', + 'sap_concur_update_expected_expense', + 'sap_concur_create_cash_advance', + 'sap_concur_create_user', + 'sap_concur_update_user', + 'sap_concur_search_users', + 'sap_concur_create_purchase_request', + 'sap_concur_upload_exchange_rates', + 'sap_concur_create_list_item', + 'sap_concur_update_list_item', + ], + }, + }, + ], + tools: { + access: [ + 'sap_concur_approve_expense_report', + 'sap_concur_associate_attendees', + 'sap_concur_create_cash_advance', + 'sap_concur_create_expected_expense', + 'sap_concur_create_expense_report', + 'sap_concur_create_list_item', + 'sap_concur_create_purchase_request', + 'sap_concur_create_quick_expense', + 'sap_concur_create_quick_expense_with_image', + 'sap_concur_create_report_comment', + 'sap_concur_create_travel_request', + 'sap_concur_create_user', + 'sap_concur_delete_expected_expense', + 'sap_concur_delete_expense', + 'sap_concur_delete_expense_report', + 'sap_concur_delete_list_item', + 'sap_concur_delete_travel_request', + 'sap_concur_delete_user', + 'sap_concur_get_allocation', + 'sap_concur_get_budget', + 'sap_concur_get_cash_advance', + 'sap_concur_upload_exchange_rates', + 'sap_concur_get_expected_expense', + 'sap_concur_get_expense', + 'sap_concur_get_expense_report', + 'sap_concur_get_itemizations', + 'sap_concur_get_itinerary', + 'sap_concur_get_list', + 'sap_concur_get_list_item', + 'sap_concur_get_purchase_request', + 'sap_concur_get_receipt', + 'sap_concur_get_receipt_status', + 'sap_concur_get_travel_profile', + 'sap_concur_get_travel_request', + 'sap_concur_get_user', + 'sap_concur_issue_cash_advance', + 'sap_concur_list_allocations', + 'sap_concur_list_attendee_associations', + 'sap_concur_list_budget_categories', + 'sap_concur_list_budgets', + 'sap_concur_list_exceptions', + 'sap_concur_list_expected_expenses', + 'sap_concur_list_expenses', + 'sap_concur_list_expense_reports', + 'sap_concur_list_itineraries', + 'sap_concur_list_lists', + 'sap_concur_list_list_items', + 'sap_concur_list_receipts', + 'sap_concur_list_report_comments', + 'sap_concur_list_reports_to_approve', + 'sap_concur_get_request_cash_advance', + 'sap_concur_list_travel_profiles_summary', + 'sap_concur_list_travel_request_comments', + 'sap_concur_list_travel_requests', + 'sap_concur_list_users', + 'sap_concur_move_travel_request', + 'sap_concur_recall_expense_report', + 'sap_concur_remove_all_attendees', + 'sap_concur_search_locations', + 'sap_concur_search_users', + 'sap_concur_send_back_expense_report', + 'sap_concur_submit_expense_report', + 'sap_concur_update_allocation', + 'sap_concur_update_expected_expense', + 'sap_concur_update_expense', + 'sap_concur_update_expense_report', + 'sap_concur_update_list_item', + 'sap_concur_update_travel_request', + 'sap_concur_update_user', + 'sap_concur_upload_receipt_image', + ], + config: { + tool: (params) => params.operation, + params: (params) => { + const auth = { + datacenter: params.datacenter || undefined, + grantType: params.grantType || undefined, + clientId: params.clientId, + clientSecret: params.clientSecret, + username: params.username || undefined, + password: params.password || undefined, + companyUuid: params.companyUuid || undefined, + } + + const limit = params.limit ? Number(params.limit) : undefined + const offset = params.offset ? Number(params.offset) : undefined + const start = params.start ? Number(params.start) : undefined + const count = params.count ? Number(params.count) : undefined + const startIndex = params.startIndex ? Number(params.startIndex) : undefined + const page = params.page ? Number(params.page) : undefined + const levelCount = params.levelCount ? Number(params.levelCount) : undefined + + switch (params.operation) { + case 'sap_concur_list_expense_reports': + return { + ...auth, + user: params.expenseReportUser || params.userId || undefined, + submitDateBefore: params.submitDateBefore || undefined, + submitDateAfter: params.submitDateAfter || undefined, + paidDateBefore: params.paidDateBefore || undefined, + paidDateAfter: params.paidDateAfter || undefined, + modifiedDateBefore: params.modifiedDateBefore || undefined, + modifiedDateAfter: params.modifiedDateAfter || undefined, + createDateBefore: params.createDateBefore || undefined, + createDateAfter: params.createDateAfter || undefined, + approvalStatusCode: params.approvalStatusCode || undefined, + paymentStatusCode: params.paymentStatusCode || undefined, + currencyCode: params.currencyCode || undefined, + approverLoginID: params.approverLoginID || undefined, + limit, + offset: params.offset ? String(params.offset) : undefined, + } + case 'sap_concur_get_expense_report': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + } + case 'sap_concur_create_expense_report': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + body: params.body, + } + case 'sap_concur_update_expense_report': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + body: params.body, + } + case 'sap_concur_delete_expense_report': + return { ...auth, reportId: params.reportId } + case 'sap_concur_submit_expense_report': + return { + ...auth, + userId: params.userId, + reportId: params.reportId, + body: params.body || undefined, + } + case 'sap_concur_recall_expense_report': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + body: params.body || undefined, + } + case 'sap_concur_approve_expense_report': + case 'sap_concur_send_back_expense_report': + return { + ...auth, + reportId: params.reportId, + body: params.body || undefined, + } + case 'sap_concur_list_reports_to_approve': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + sort: params.reportsToApproveSort || undefined, + order: params.reportsToApproveOrder || undefined, + includeDelegateApprovals: toBool(params.includeDelegateApprovals), + } + case 'sap_concur_list_expenses': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + } + case 'sap_concur_get_expense': + case 'sap_concur_get_itemizations': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + expenseId: params.expenseId, + } + case 'sap_concur_update_expense': + return { + ...auth, + reportId: params.reportId, + expenseId: params.expenseId, + body: params.body, + } + case 'sap_concur_delete_expense': + return { + ...auth, + reportId: params.reportId, + expenseId: params.expenseId, + } + case 'sap_concur_list_allocations': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + expenseId: params.expenseId, + } + case 'sap_concur_get_allocation': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + allocationId: params.allocationId, + } + case 'sap_concur_update_allocation': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + allocationId: params.allocationId, + body: params.body, + } + case 'sap_concur_list_attendee_associations': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + expenseId: params.expenseId, + } + case 'sap_concur_associate_attendees': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + expenseId: params.expenseId, + body: params.body, + } + case 'sap_concur_remove_all_attendees': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + expenseId: params.expenseId, + } + case 'sap_concur_list_report_comments': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + includeAllComments: toBool(params.includeAllComments), + } + case 'sap_concur_create_report_comment': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + comment: params.comment, + } + case 'sap_concur_list_exceptions': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + reportId: params.reportId, + } + case 'sap_concur_create_quick_expense': + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + body: params.body, + } + case 'sap_concur_list_receipts': + return { ...auth, userId: params.userId } + case 'sap_concur_get_receipt': + case 'sap_concur_get_receipt_status': + return { ...auth, receiptId: params.receiptId } + case 'sap_concur_list_travel_requests': + return { + ...auth, + view: params.view || undefined, + limit, + start, + userId: params.travelRequestUserId || undefined, + approvedBefore: params.travelRequestApprovedBefore || undefined, + approvedAfter: params.travelRequestApprovedAfter || undefined, + modifiedBefore: params.travelRequestModifiedBefore || undefined, + modifiedAfter: params.travelRequestModifiedAfter || undefined, + sortField: params.travelRequestSortField || undefined, + sortOrder: + params.travelRequestSortOrder === 'asc' || params.travelRequestSortOrder === 'desc' + ? params.travelRequestSortOrder + : undefined, + } + case 'sap_concur_get_travel_request': + case 'sap_concur_delete_travel_request': + return { + ...auth, + requestUuid: params.requestUuid, + userId: params.travelRequestUserId || undefined, + } + case 'sap_concur_create_travel_request': + return { ...auth, body: params.body, userId: params.travelRequestUserId || undefined } + case 'sap_concur_update_travel_request': + return { ...auth, requestUuid: params.requestUuid, body: params.body } + case 'sap_concur_move_travel_request': + return { + ...auth, + requestUuid: params.requestUuid, + action: params.action, + body: params.body || undefined, + userId: params.travelRequestUserId || undefined, + } + case 'sap_concur_list_travel_request_comments': + return { ...auth, requestUuid: params.requestUuid } + case 'sap_concur_list_expected_expenses': + return { + ...auth, + requestUuid: params.requestUuid, + userId: params.expectedExpenseUserId || undefined, + } + case 'sap_concur_get_request_cash_advance': + return { ...auth, cashAdvanceUuid: params.cashAdvanceUuid } + case 'sap_concur_create_expected_expense': + return { + ...auth, + requestUuid: params.requestUuid, + body: params.body, + userId: params.expectedExpenseUserId || undefined, + } + case 'sap_concur_get_expected_expense': + case 'sap_concur_delete_expected_expense': + return { + ...auth, + expenseUuid: params.expenseUuid, + userId: params.expectedExpenseUserId || undefined, + } + case 'sap_concur_update_expected_expense': + return { + ...auth, + expenseUuid: params.expenseUuid, + body: params.body, + userId: params.expectedExpenseUserId || undefined, + } + case 'sap_concur_create_cash_advance': + return { ...auth, body: params.body } + case 'sap_concur_get_cash_advance': + return { ...auth, cashAdvanceId: params.cashAdvanceId } + case 'sap_concur_issue_cash_advance': + return { + ...auth, + cashAdvanceId: params.cashAdvanceId, + body: params.body || undefined, + } + case 'sap_concur_list_itineraries': + return { + ...auth, + startDate: params.startDate || undefined, + endDate: params.endDate || undefined, + bookingType: params.bookingType || undefined, + useridType: params.useridType || undefined, + useridValue: params.useridValue || undefined, + itemsPerPage: params.itineraryItemsPerPage + ? Number(params.itineraryItemsPerPage) + : undefined, + page: params.itineraryPage ? Number(params.itineraryPage) : undefined, + includeMetadata: toBool(params.includeMetadata), + includeCanceledTrips: toBool(params.includeCanceledTrips), + createdAfterDate: params.createdAfterDate || undefined, + createdBeforeDate: params.createdBeforeDate || undefined, + lastModifiedDate: params.itineraryLastModifiedDate || undefined, + } + case 'sap_concur_get_itinerary': + return { + ...auth, + tripId: params.tripId, + useridType: params.useridType || undefined, + useridValue: params.useridValue || undefined, + systemFormat: params.systemFormat || undefined, + } + case 'sap_concur_list_users': + return { + ...auth, + count, + cursor: params.usersCursor || undefined, + attributes: params.attributes || undefined, + excludedAttributes: params.excludedAttributes || undefined, + } + case 'sap_concur_get_user': + return { + ...auth, + userUuid: params.userUuid, + attributes: params.attributes || undefined, + excludedAttributes: params.excludedAttributes || undefined, + } + case 'sap_concur_delete_user': + return { ...auth, userUuid: params.userUuid } + case 'sap_concur_create_user': + return { ...auth, body: params.body } + case 'sap_concur_update_user': + return { ...auth, userUuid: params.userUuid, body: params.body } + case 'sap_concur_search_users': + return { ...auth, body: params.body } + case 'sap_concur_list_lists': + return { + ...auth, + page, + sortBy: params.sortBy || undefined, + sortDirection: params.sortDirection || undefined, + value: params.value || undefined, + categoryType: params.categoryType || undefined, + isDeleted: toBool(params.isDeleted), + levelCount, + } + case 'sap_concur_get_list': + return { ...auth, listId: params.listId } + case 'sap_concur_list_list_items': + return { + ...auth, + listId: params.listId, + page, + sortBy: params.sortBy || undefined, + sortDirection: params.sortDirection || undefined, + hasChildren: toBool(params.hasChildren), + isDeleted: toBool(params.isDeleted), + shortCode: params.shortCode || undefined, + value: params.value || undefined, + shortCodeOrValue: params.shortCodeOrValue || undefined, + } + case 'sap_concur_get_list_item': + return { + ...auth, + itemId: params.itemId, + } + case 'sap_concur_list_budgets': + return { + ...auth, + adminView: toBool(params.adminView), + offset, + responseSchema: params.responseSchema || undefined, + } + case 'sap_concur_get_budget': + return { ...auth, budgetId: params.budgetId } + case 'sap_concur_list_budget_categories': + return { ...auth } + case 'sap_concur_upload_exchange_rates': + return { ...auth, body: params.body } + case 'sap_concur_create_purchase_request': + return { ...auth, body: params.body } + case 'sap_concur_get_purchase_request': + return { ...auth, purchaseRequestId: params.purchaseRequestId } + case 'sap_concur_create_list_item': + return { ...auth, body: params.body } + case 'sap_concur_update_list_item': + return { ...auth, itemId: params.itemId, body: params.body } + case 'sap_concur_delete_list_item': + return { ...auth, itemId: params.itemId } + case 'sap_concur_get_travel_profile': + return { + ...auth, + useridType: params.useridType || undefined, + useridValue: params.useridValue || undefined, + } + case 'sap_concur_list_travel_profiles_summary': + return { + ...auth, + lastModifiedDate: params.lastModifiedDate, + page: params.travelProfilePage ? Number(params.travelProfilePage) : undefined, + itemsPerPage: params.itemsPerPage ? Number(params.itemsPerPage) : undefined, + travelConfigs: params.travelConfigs || undefined, + } + case 'sap_concur_search_locations': + return { + ...auth, + searchText: params.searchText || undefined, + locCode: params.locCode || undefined, + locationNameId: params.locationNameId || undefined, + locationNameKey: params.locationNameKey ? Number(params.locationNameKey) : undefined, + countryCode: params.countryCode || undefined, + subdivisionCode: params.subdivisionCode || undefined, + adminRegionId: params.adminRegionId || undefined, + } + case 'sap_concur_upload_receipt_image': { + const normalizedReceipt = normalizeFileInput(params.receipt, { single: true }) as + | UserFileLike + | undefined + return { + ...auth, + userId: params.userId, + receipt: normalizedReceipt, + forwardId: params.forwardId || undefined, + } + } + case 'sap_concur_create_quick_expense_with_image': { + const normalizedReceipt = normalizeFileInput(params.receipt, { single: true }) as + | UserFileLike + | undefined + return { + ...auth, + userId: params.userId, + contextType: params.contextType, + receipt: normalizedReceipt, + body: params.body, + } + } + default: + throw new Error(`Unsupported SAP Concur operation: ${params.operation}`) + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + datacenter: { type: 'string', description: 'Concur datacenter base URL' }, + grantType: { type: 'string', description: 'OAuth grant type' }, + clientId: { type: 'string', description: 'OAuth client ID' }, + clientSecret: { type: 'string', description: 'OAuth client secret' }, + username: { type: 'string', description: 'Username (password grant only)' }, + password: { type: 'string', description: 'Password (password grant only)' }, + companyUuid: { type: 'string', description: 'Company UUID for multi-company tokens' }, + userId: { type: 'string', description: 'Concur user UUID' }, + contextType: { + type: 'string', + description: 'Access context (TRAVELER/MANAGER, or TRAVELER/PROXY for allocations)', + }, + reportId: { type: 'string', description: 'Expense report ID' }, + expenseId: { type: 'string', description: 'Expense entry ID' }, + allocationId: { type: 'string', description: 'Allocation ID' }, + expenseReportUser: { + type: 'string', + description: 'v3 list expense reports — user filter (login id)', + }, + submitDateBefore: { + type: 'string', + description: 'v3 list expense reports — submit date before', + }, + submitDateAfter: { type: 'string', description: 'v3 list expense reports — submit date after' }, + paidDateBefore: { type: 'string', description: 'v3 list expense reports — paid date before' }, + paidDateAfter: { type: 'string', description: 'v3 list expense reports — paid date after' }, + modifiedDateBefore: { + type: 'string', + description: 'v3 list expense reports — modified date before', + }, + modifiedDateAfter: { + type: 'string', + description: 'v3 list expense reports — modified date after', + }, + createDateBefore: { + type: 'string', + description: 'v3 list expense reports — create date before', + }, + createDateAfter: { + type: 'string', + description: 'v3 list expense reports — create date after', + }, + approvalStatusCode: { + type: 'string', + description: 'v3 list expense reports — approval status code', + }, + paymentStatusCode: { + type: 'string', + description: 'v3 list expense reports — payment status code', + }, + currencyCode: { type: 'string', description: 'v3 list expense reports — currency code' }, + approverLoginID: { type: 'string', description: 'v3 list expense reports — approver login id' }, + comment: { type: 'string', description: 'Comment text' }, + receiptId: { type: 'string', description: 'Receipt image ID' }, + requestUuid: { type: 'string', description: 'Travel request UUID' }, + view: { type: 'string', description: 'Travel request view filter' }, + travelRequestUserId: { + type: 'string', + description: 'User UUID for travel request impersonation/filter', + }, + travelRequestApprovedBefore: { type: 'string', description: 'Travel requests approved before' }, + travelRequestApprovedAfter: { type: 'string', description: 'Travel requests approved after' }, + travelRequestModifiedBefore: { type: 'string', description: 'Travel requests modified before' }, + travelRequestModifiedAfter: { type: 'string', description: 'Travel requests modified after' }, + travelRequestSortField: { type: 'string', description: 'Travel requests sort field' }, + travelRequestSortOrder: { type: 'string', description: 'Travel requests sort order' }, + action: { type: 'string', description: 'Travel request workflow action' }, + expectedExpenseUserId: { + type: 'string', + description: 'Expected expense impersonation user UUID', + }, + expenseUuid: { type: 'string', description: 'Expected expense UUID' }, + cashAdvanceId: { type: 'string', description: 'Cash advance ID' }, + cashAdvanceUuid: { type: 'string', description: 'Cash advance UUID (travel request scope)' }, + tripId: { type: 'string', description: 'Trip/itinerary ID' }, + startDate: { type: 'string', description: 'Itinerary start date filter' }, + endDate: { type: 'string', description: 'Itinerary end date filter' }, + bookingType: { type: 'string', description: 'Itinerary booking type filter' }, + systemFormat: { type: 'string', description: 'Itinerary system format (e.g., GDS)' }, + itineraryItemsPerPage: { type: 'number', description: 'Itinerary items per page' }, + itineraryPage: { type: 'number', description: 'Itinerary page number' }, + includeMetadata: { type: 'boolean', description: 'Include itinerary paging metadata' }, + includeCanceledTrips: { type: 'boolean', description: 'Include canceled trips' }, + createdAfterDate: { type: 'string', description: 'Itinerary created-after date' }, + createdBeforeDate: { type: 'string', description: 'Itinerary created-before date' }, + itineraryLastModifiedDate: { type: 'string', description: 'Itinerary last-modified date' }, + userUuid: { type: 'string', description: 'User identity UUID' }, + count: { type: 'number', description: 'SCIM count' }, + usersCursor: { type: 'string', description: 'SCIM v4.1 cursor for /users' }, + attributes: { type: 'string', description: 'SCIM attributes filter' }, + excludedAttributes: { type: 'string', description: 'SCIM excluded attributes' }, + listId: { type: 'string', description: 'Custom list ID' }, + itemId: { type: 'string', description: 'List item v4 UUID' }, + sortBy: { type: 'string', description: 'Sort field for v4 lists/items endpoints' }, + sortDirection: { type: 'string', description: 'Sort direction: asc or desc' }, + value: { type: 'string', description: 'Filter by value/name for v4 lists/items endpoints' }, + categoryType: { type: 'string', description: 'List category.type filter' }, + isDeleted: { type: 'boolean', description: 'Include deleted lists/items' }, + levelCount: { type: 'number', description: 'Filter lists by level count' }, + hasChildren: { type: 'boolean', description: 'Filter list items that have children' }, + shortCode: { type: 'string', description: 'Filter list items by short code' }, + shortCodeOrValue: { type: 'string', description: 'Filter list items by short code or value' }, + budgetId: { type: 'string', description: 'Budget header ID' }, + adminView: { type: 'boolean', description: 'Return all admin-visible budgets' }, + responseSchema: { type: 'string', description: 'Budget response schema (COMPACT)' }, + purchaseRequestId: { type: 'string', description: 'Purchase request ID' }, + limit: { type: 'number', description: 'Max records per page' }, + offset: { type: 'number', description: 'Page offset' }, + start: { type: 'number', description: 'Page start cursor (offset)' }, + body: { type: 'json', description: 'JSON request body' }, + useridType: { type: 'string', description: 'Travel profile identifier type' }, + useridValue: { type: 'string', description: 'Travel profile identifier value' }, + lastModifiedDate: { type: 'string', description: 'Required ISO date for profile summary' }, + page: { type: 'number', description: 'Page number (lists/list_items)' }, + travelProfilePage: { type: 'number', description: 'Profile summary page number' }, + itemsPerPage: { type: 'number', description: 'Profile summary items per page' }, + travelConfigs: { type: 'string', description: 'Comma-separated travel config ids' }, + searchText: { type: 'string', description: 'Locations v5 free-text search' }, + locCode: { type: 'string', description: 'Locations v5 location code' }, + locationNameId: { type: 'string', description: 'Locations v5 location name id' }, + locationNameKey: { type: 'number', description: 'Locations v5 numeric location name key' }, + countryCode: { type: 'string', description: 'Locations v5 ISO 3166-1 country code' }, + subdivisionCode: { type: 'string', description: 'Locations v5 ISO 3166-2 subdivision code' }, + adminRegionId: { type: 'string', description: 'Locations v5 administrative region id' }, + receipt: { type: 'json', description: 'Receipt image file (canonical param)' }, + forwardId: { type: 'string', description: 'Optional dedup id for receipt upload' }, + reportsToApproveSort: { + type: 'string', + description: 'Sort field for reportsToApprove (e.g., reportDate)', + }, + reportsToApproveOrder: { type: 'string', description: 'Sort order: asc or desc' }, + includeDelegateApprovals: { + type: 'boolean', + description: 'Include reports the caller can approve as a delegate', + }, + includeAllComments: { + type: 'boolean', + description: 'Include comments from all expenses in the report', + }, + }, + outputs: { + success: { type: 'boolean', description: 'Whether the operation succeeded' }, + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { type: 'json', description: 'Concur API response payload' }, + }, +} diff --git a/apps/sim/blocks/blocks/sap_s4hana.ts b/apps/sim/blocks/blocks/sap_s4hana.ts index 30ff9b900b4..b5b985b461b 100644 --- a/apps/sim/blocks/blocks/sap_s4hana.ts +++ b/apps/sim/blocks/blocks/sap_s4hana.ts @@ -71,6 +71,25 @@ export const SapS4HanaBlock: BlockConfig = { title: '$filter', type: 'long-input', placeholder: "BusinessPartnerCategory eq '1'", + wandConfig: { + enabled: true, + prompt: `Generate an OData v2 $filter expression for SAP S/4HANA based on the user's request. + +Rules: +- String literals are single-quoted, e.g. eq '1010' +- Combine clauses with 'and' / 'or' +- Common operators: eq, ne, gt, ge, lt, le +- Date/time literals use datetime'YYYY-MM-DDTHH:MM:SS' +- Functions: substringof('x', Field), startswith(Field, 'x'), endswith(Field, 'x') + +Examples: +- BusinessPartnerCategory eq '1' and Country eq 'US' +- CreationDate gt datetime'2024-01-01T00:00:00' +- substringof('ACME', OrganizationBPName1) + +Return ONLY the $filter expression - no explanations, no extra text.`, + placeholder: 'Describe the filter you want (e.g., "people in the US created this year")', + }, condition: { field: 'operation', value: [ @@ -405,6 +424,22 @@ export const SapS4HanaBlock: BlockConfig = { placeholder: '[{"Material":"TG11","RequestedQuantity":"1"}]', condition: { field: 'operation', value: 'sap_s4hana_create_sales_order' }, required: true, + wandConfig: { + enabled: true, + prompt: `Generate a JSON array of SAP S/4HANA A_SalesOrderItem objects for a deep-insert under to_Item. + +Rules: +- Output a JSON array, each element an item object +- Common fields: Material (string), RequestedQuantity (string-decimal), RequestedQuantityUnit (e.g., "PC"), Plant (4-char), SalesOrderItemCategory +- Numbers in OData v2 decimals are passed as strings (e.g., "5", "10.5") + +Examples: +- [{"Material":"TG11","RequestedQuantity":"1"}] +- [{"Material":"MZ-FG-M100","RequestedQuantity":"5","RequestedQuantityUnit":"PC","Plant":"1010"}] + +Return ONLY the JSON array - no explanations, no extra text.`, + placeholder: 'Describe the items (e.g., "5 units of material TG11 from plant 1010")', + }, }, { id: 'salesOrderBody', @@ -456,7 +491,7 @@ export const SapS4HanaBlock: BlockConfig = { id: 'purchaseRequisition', title: 'PurchaseRequisition', type: 'short-input', - placeholder: '10000000', + placeholder: '0010000000', condition: { field: 'operation', value: ['sap_s4hana_get_purchase_requisition', 'sap_s4hana_update_purchase_requisition'], @@ -485,7 +520,7 @@ export const SapS4HanaBlock: BlockConfig = { id: 'purchaseRequisitionBody', title: 'Additional Fields (JSON)', type: 'code', - placeholder: '{"PurchaseRequisitionDescription":"Office supplies"}', + placeholder: '{"PurReqnDescription":"Office supplies"}', condition: { field: 'operation', value: 'sap_s4hana_create_purchase_requisition' }, mode: 'advanced', }, diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 54b2312c086..f5d9b40fd3d 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -171,6 +171,7 @@ import { RouterBlock, RouterV2Block } from '@/blocks/blocks/router' import { RssBlock } from '@/blocks/blocks/rss' import { S3Block } from '@/blocks/blocks/s3' import { SalesforceBlock } from '@/blocks/blocks/salesforce' +import { SapConcurBlock } from '@/blocks/blocks/sap_concur' import { SapS4HanaBlock } from '@/blocks/blocks/sap_s4hana' import { ScheduleBlock } from '@/blocks/blocks/schedule' import { SearchBlock } from '@/blocks/blocks/search' @@ -424,6 +425,7 @@ export const registry: Record = { rss: RssBlock, s3: S3Block, salesforce: SalesforceBlock, + sap_concur: SapConcurBlock, sap_s4hana: SapS4HanaBlock, schedule: ScheduleBlock, search: SearchBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index c4bc260742b..4092f8c10a7 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -4141,6 +4141,25 @@ export function SapS4HanaIcon(props: SVGProps) { ) } +export function SapConcurIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function ServiceNowIcon(props: SVGProps) { return ( diff --git a/apps/sim/lib/core/security/input-validation.server.ts b/apps/sim/lib/core/security/input-validation.server.ts index ed23140ea46..90c65eca62e 100644 --- a/apps/sim/lib/core/security/input-validation.server.ts +++ b/apps/sim/lib/core/security/input-validation.server.ts @@ -217,15 +217,22 @@ export interface SecureFetchOptions { export class SecureFetchHeaders { private headers: Map + private setCookies: string[] - constructor(headers: Record) { + constructor(headers: Record, setCookies: string[] = []) { this.headers = new Map(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])) + this.setCookies = setCookies } get(name: string): string | null { return this.headers.get(name.toLowerCase()) ?? null } + /** Returns the raw `Set-Cookie` header values as an array. Each entry is one cookie. */ + getSetCookie(): string[] { + return [...this.setCookies] + } + toRecord(): Record { const record: Record = {} for (const [key, value] of this.headers) { @@ -384,11 +391,21 @@ export async function secureFetchWithPinnedIP( const bodyBuffer = Buffer.concat(chunks) const body = bodyBuffer.toString('utf-8') const headersRecord: Record = {} + let setCookieArray: string[] = [] for (const [key, value] of Object.entries(res.headers)) { - if (typeof value === 'string') { - headersRecord[key.toLowerCase()] = value + const lowerKey = key.toLowerCase() + if (lowerKey === 'set-cookie') { + if (Array.isArray(value)) { + setCookieArray = value + headersRecord[lowerKey] = value.join(', ') + } else if (typeof value === 'string') { + setCookieArray = [value] + headersRecord[lowerKey] = value + } + } else if (typeof value === 'string') { + headersRecord[lowerKey] = value } else if (Array.isArray(value)) { - headersRecord[key.toLowerCase()] = value.join(', ') + headersRecord[lowerKey] = value.join(', ') } } @@ -396,7 +413,7 @@ export async function secureFetchWithPinnedIP( ok: statusCode >= 200 && statusCode < 300, status: statusCode, statusText: res.statusMessage || '', - headers: new SecureFetchHeaders(headersRecord), + headers: new SecureFetchHeaders(headersRecord, setCookieArray), text: async () => body, json: async () => JSON.parse(body), arrayBuffer: async () => diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index e56fb11b9e2..8da147da4e7 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2270,6 +2270,78 @@ import { salesforceUpdateOpportunityTool, salesforceUpdateTaskTool, } from '@/tools/salesforce' +import { + approveExpenseReportTool as sapConcurApproveExpenseReportTool, + associateAttendeesTool as sapConcurAssociateAttendeesTool, + createCashAdvanceTool as sapConcurCreateCashAdvanceTool, + createExpectedExpenseTool as sapConcurCreateExpectedExpenseTool, + createExpenseReportTool as sapConcurCreateExpenseReportTool, + createListItemTool as sapConcurCreateListItemTool, + createPurchaseRequestTool as sapConcurCreatePurchaseRequestTool, + createQuickExpenseTool as sapConcurCreateQuickExpenseTool, + createQuickExpenseWithImageTool as sapConcurCreateQuickExpenseWithImageTool, + createReportCommentTool as sapConcurCreateReportCommentTool, + createTravelRequestTool as sapConcurCreateTravelRequestTool, + createUserTool as sapConcurCreateUserTool, + deleteExpectedExpenseTool as sapConcurDeleteExpectedExpenseTool, + deleteExpenseReportTool as sapConcurDeleteExpenseReportTool, + deleteExpenseTool as sapConcurDeleteExpenseTool, + deleteListItemTool as sapConcurDeleteListItemTool, + deleteTravelRequestTool as sapConcurDeleteTravelRequestTool, + deleteUserTool as sapConcurDeleteUserTool, + getAllocationTool as sapConcurGetAllocationTool, + getBudgetTool as sapConcurGetBudgetTool, + getCashAdvanceTool as sapConcurGetCashAdvanceTool, + 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 = { 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, diff --git a/apps/sim/tools/sap_concur/approve_expense_report.ts b/apps/sim/tools/sap_concur/approve_expense_report.ts new file mode 100644 index 00000000000..9ab472482d1 --- /dev/null +++ b/apps/sim/tools/sap_concur/approve_expense_report.ts @@ -0,0 +1,95 @@ +import type { ApproveExpenseReportParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const approveExpenseReportTool: ToolConfig< + ApproveExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_approve_expense_report', + name: 'SAP Concur Approve Expense Report', + description: + 'Approve an expense report as a manager (PATCH /expensereports/v4/reports/{reportId}/approve). Required body field: comment.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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)' }, + }, +} diff --git a/apps/sim/tools/sap_concur/associate_attendees.ts b/apps/sim/tools/sap_concur/associate_attendees.ts new file mode 100644 index 00000000000..ae75a711c16 --- /dev/null +++ b/apps/sim/tools/sap_concur/associate_attendees.ts @@ -0,0 +1,123 @@ +import type { AssociateAttendeesParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const associateAttendeesTool: ToolConfig = + { + id: 'sap_concur_associate_attendees', + name: 'SAP Concur Associate Attendees', + description: + 'Associate attendees with an expense (POST /expensereports/v4/users/{userId}/context/{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, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/create_cash_advance.ts b/apps/sim/tools/sap_concur/create_cash_advance.ts new file mode 100644 index 00000000000..7297eb214c2 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_cash_advance.ts @@ -0,0 +1,90 @@ +import type { CreateCashAdvanceParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createCashAdvanceTool: ToolConfig = { + id: 'sap_concur_create_cash_advance', + name: 'SAP Concur Create Cash Advance', + description: 'Create a cash advance (POST /cashadvance/v4.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, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_expected_expense.ts b/apps/sim/tools/sap_concur/create_expected_expense.ts new file mode 100644 index 00000000000..893da024313 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_expected_expense.ts @@ -0,0 +1,179 @@ +import type { CreateExpectedExpenseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createExpectedExpenseTool: ToolConfig< + CreateExpectedExpenseParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_create_expected_expense', + name: 'SAP Concur Create Expected Expense', + description: + 'Create an expected expense on a travel request (POST /travelrequest/v4/requests/{requestUuid}/expenses).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 = {} + if (params.userId?.trim()) query.userId = params.userId.trim() + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests/${encodeURIComponent(requestUuid)}/expenses`, + method: 'POST', + body: params.body, + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Created expected expense payload', + properties: { + id: { type: 'string', description: 'Expected expense identifier', optional: true }, + href: { type: 'string', description: 'Self-link to the resource', optional: true }, + expenseType: { + type: 'json', + description: 'Expense type {id, name}', + optional: true, + }, + transactionDate: { + type: 'string', + description: 'Transaction date', + optional: true, + }, + transactionAmount: { + type: 'json', + description: 'Transaction amount {value, currencyCode}', + optional: true, + }, + postedAmount: { + type: 'json', + description: 'Posted amount {value, currencyCode}', + optional: true, + }, + approvedAmount: { + type: 'json', + description: 'Approved amount {value, currencyCode}', + optional: true, + }, + remainingAmount: { + type: 'json', + description: 'Remaining amount on the expected expense', + optional: true, + }, + businessPurpose: { + type: 'string', + description: 'Business purpose of the expense', + optional: true, + }, + location: { + type: 'json', + description: + 'Location {id, name, city, countryCode, countrySubDivisionCode, iataCode, locationType}', + optional: true, + }, + exchangeRate: { + type: 'json', + description: 'Exchange rate {value, operation}', + optional: true, + }, + allocations: { + type: 'json', + description: + 'Budget allocations array (allocationId, allocationAmount, approvedAmount, postedAmount, expenseId, percentEdited, systemAllocation, percentage)', + optional: true, + }, + tripData: { + type: 'json', + description: + 'Trip data {agencyBooked, selfBooked, tripType (ONE_WAY|ROUND_TRIP), legs[{id, returnLeg, startDate, startTime, startLocationDetail, startLocation, endLocation, class {code,value}, travelExceptionReasonCodes}], segmentType {category, code}}', + optional: true, + }, + parentRequest: { + type: 'json', + description: 'Parent travel request resource link {href, id}', + optional: true, + }, + comments: { + type: 'json', + description: 'Comments sub-resource link {href, id}', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_expense_report.ts b/apps/sim/tools/sap_concur/create_expense_report.ts new file mode 100644 index 00000000000..6927c018b0b --- /dev/null +++ b/apps/sim/tools/sap_concur/create_expense_report.ts @@ -0,0 +1,109 @@ +import type { CreateExpenseReportParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createExpenseReportTool: ToolConfig< + CreateExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_create_expense_report', + name: 'SAP Concur Create Expense Report', + description: + 'Create an expense report (POST /expensereports/v4/users/{userId}/context/{contextType}/reports — supported contexts: TRAVELER, PROXY). Required body fields: name, policyId.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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' }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_list_item.ts b/apps/sim/tools/sap_concur/create_list_item.ts new file mode 100644 index 00000000000..3c203a3b699 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_list_item.ts @@ -0,0 +1,121 @@ +import type { CreateListItemParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createListItemTool: ToolConfig = { + id: 'sap_concur_create_list_item', + name: 'SAP Concur Create List Item', + description: 'Create a list item (POST /list/v4/items).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_purchase_request.ts b/apps/sim/tools/sap_concur/create_purchase_request.ts new file mode 100644 index 00000000000..11d40967d3b --- /dev/null +++ b/apps/sim/tools/sap_concur/create_purchase_request.ts @@ -0,0 +1,115 @@ +import type { CreatePurchaseRequestParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createPurchaseRequestTool: ToolConfig< + CreatePurchaseRequestParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_create_purchase_request', + name: 'SAP Concur Create Purchase Request', + description: 'Create a purchase request (POST /purchaserequest/v4/purchaserequests).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_quick_expense.ts b/apps/sim/tools/sap_concur/create_quick_expense.ts new file mode 100644 index 00000000000..7f4bb8593b1 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_quick_expense.ts @@ -0,0 +1,110 @@ +import type { CreateQuickExpenseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createQuickExpenseTool: ToolConfig = + { + id: 'sap_concur_create_quick_expense', + name: 'SAP Concur Create Quick Expense', + description: + 'Create a quick expense (POST /quickexpense/v4/users/{userId}/context/TRAVELER/quickexpenses).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/create_quick_expense_with_image.ts b/apps/sim/tools/sap_concur/create_quick_expense_with_image.ts new file mode 100644 index 00000000000..3c106b917d4 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_quick_expense_with_image.ts @@ -0,0 +1,123 @@ +import type { + CreateQuickExpenseWithImageParams, + SapConcurProxyResponse, +} from '@/tools/sap_concur/types' +import { SAP_CONCUR_UPLOAD_URL } from '@/tools/sap_concur/upload_receipt_image' +import { + baseProxyBody, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createQuickExpenseWithImageTool: ToolConfig< + CreateQuickExpenseWithImageParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_create_quick_expense_with_image', + name: 'SAP Concur Create Quick Expense With Image', + description: + 'Create a quick expense with an attached image (POST /quickexpense/v4/users/{userId}/context/{contextType}/quickexpenses/image).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_report_comment.ts b/apps/sim/tools/sap_concur/create_report_comment.ts new file mode 100644 index 00000000000..f03271695c0 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_report_comment.ts @@ -0,0 +1,119 @@ +import type { CreateReportCommentParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createReportCommentTool: ToolConfig< + CreateReportCommentParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_create_report_comment', + name: 'SAP Concur Create Report Comment', + description: + 'Create a comment on a report (POST /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/comments).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_travel_request.ts b/apps/sim/tools/sap_concur/create_travel_request.ts new file mode 100644 index 00000000000..78187817c20 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_travel_request.ts @@ -0,0 +1,264 @@ +import type { CreateTravelRequestParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createTravelRequestTool: ToolConfig< + CreateTravelRequestParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_create_travel_request', + name: 'SAP Concur Create Travel Request', + description: 'Create a travel request (POST /travelrequest/v4/requests).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 = {} + if (params.userId) query.userId = params.userId + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests`, + method: 'POST', + body: params.body, + query: Object.keys(query).length > 0 ? query : undefined, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Created travel request payload', + properties: { + id: { type: 'string', description: 'Travel request UUID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + requestId: { + type: 'string', + description: 'Public-facing request ID (4-6 alphanumeric characters)', + optional: true, + }, + name: { type: 'string', description: 'Request name', optional: true }, + businessPurpose: { type: 'string', description: 'Business purpose', optional: true }, + comment: { type: 'string', description: 'Last attached comment', optional: true }, + creationDate: { type: 'string', description: 'Creation timestamp', optional: true }, + lastModified: { + type: 'string', + description: 'Last modification timestamp', + optional: true, + }, + submitDate: { type: 'string', description: 'Last submission timestamp', optional: true }, + startDate: { type: 'string', description: 'Trip start date (ISO 8601)', optional: true }, + endDate: { type: 'string', description: 'Trip end date (ISO 8601)', optional: true }, + startTime: { type: 'string', description: 'Trip start time (HH:mm)', optional: true }, + endTime: { type: 'string', description: 'Trip end time (HH:mm)', optional: true }, + approved: { + type: 'boolean', + description: 'Whether the request is approved', + optional: true, + }, + pendingApproval: { type: 'boolean', description: 'Pending approval flag', optional: true }, + closed: { type: 'boolean', description: 'Closed flag', optional: true }, + everSentBack: { type: 'boolean', description: 'Ever-sent-back flag', optional: true }, + canceledPostApproval: { + type: 'boolean', + description: 'Canceled after approval flag', + optional: true, + }, + approvalStatus: { + type: 'json', + description: 'Approval status', + optional: true, + properties: { + code: { + type: 'string', + description: 'Status code (NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK)', + optional: true, + }, + name: { type: 'string', description: 'Localized status name', optional: true }, + }, + }, + owner: { + type: 'json', + description: 'Travel request owner', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { type: 'string', description: 'Owner first name', optional: true }, + lastName: { type: 'string', description: 'Owner last name', optional: true }, + }, + }, + approver: { + type: 'json', + description: 'Approver assigned to the request', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { type: 'string', description: 'Approver first name', optional: true }, + lastName: { type: 'string', description: 'Approver last name', optional: true }, + }, + }, + policy: { + type: 'json', + description: 'Resource link to the applicable policy', + optional: true, + properties: { + id: { type: 'string', description: 'Policy ID', optional: true }, + href: { type: 'string', description: 'Policy hyperlink', optional: true }, + }, + }, + type: { + type: 'json', + description: 'Request type', + optional: true, + properties: { + code: { type: 'string', description: 'Request type code', optional: true }, + label: { type: 'string', description: 'Request type label', optional: true }, + }, + }, + mainDestination: { + type: 'json', + description: 'Main destination of the trip', + optional: true, + properties: { + city: { type: 'string', description: 'City', optional: true }, + countryCode: { type: 'string', description: 'ISO country code', optional: true }, + countrySubDivisionCode: { + type: 'string', + description: 'ISO country sub-division code', + optional: true, + }, + name: { type: 'string', description: 'Destination name', optional: true }, + }, + }, + totalApprovedAmount: { + type: 'json', + description: 'Total approved amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + totalPostedAmount: { + type: 'json', + description: 'Total posted amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + totalRemainingAmount: { + type: 'json', + description: 'Total remaining amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + operations: { + type: 'array', + description: 'Available workflow actions', + optional: true, + items: { + type: 'json', + properties: { + rel: { type: 'string', description: 'Operation name', optional: true }, + href: { type: 'string', description: 'Operation URL', optional: true }, + }, + }, + }, + expenses: { + type: 'array', + description: 'Expected expenses attached to the request', + optional: true, + items: { type: 'json' }, + }, + highestExceptionLevel: { + type: 'string', + description: 'Highest exception level (NONE, WARNING, ERROR)', + optional: true, + }, + travelAgency: { + type: 'json', + description: 'Travel agency reference', + optional: true, + properties: { + id: { type: 'string', description: 'Agency identifier', optional: true }, + href: { type: 'string', description: 'Agency URL', optional: true }, + template: { type: 'string', description: 'Template URL', optional: true }, + }, + }, + custom1: { type: 'json', description: 'Custom field 1', optional: true }, + custom2: { type: 'json', description: 'Custom field 2', optional: true }, + custom3: { type: 'json', description: 'Custom field 3', optional: true }, + custom4: { type: 'json', description: 'Custom field 4', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/create_user.ts b/apps/sim/tools/sap_concur/create_user.ts new file mode 100644 index 00000000000..17558b168b8 --- /dev/null +++ b/apps/sim/tools/sap_concur/create_user.ts @@ -0,0 +1,85 @@ +import type { CreateUserParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + scimUserOutputProperties, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const createUserTool: ToolConfig = { + id: 'sap_concur_create_user', + name: 'SAP Concur Create User', + description: 'Create a new user identity (POST /profile/identity/v4.1/Users).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/delete_expected_expense.ts b/apps/sim/tools/sap_concur/delete_expected_expense.ts new file mode 100644 index 00000000000..dc362b87e31 --- /dev/null +++ b/apps/sim/tools/sap_concur/delete_expected_expense.ts @@ -0,0 +1,100 @@ +import type { DeleteExpectedExpenseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteExpectedExpenseTool: ToolConfig< + DeleteExpectedExpenseParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_delete_expected_expense', + name: 'SAP Concur Delete Expected Expense', + description: 'Delete an expected expense (DELETE /travelrequest/v4/expenses/{expenseUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 = {} + if (params.userId?.trim()) query.userId = params.userId.trim() + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/expenses/${encodeURIComponent(expenseUuid)}`, + method: 'DELETE', + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Returns boolean true on 200 OK when the expected expense is deleted.', + properties: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/delete_expense.ts b/apps/sim/tools/sap_concur/delete_expense.ts new file mode 100644 index 00000000000..3940f841fc4 --- /dev/null +++ b/apps/sim/tools/sap_concur/delete_expense.ts @@ -0,0 +1,96 @@ +import type { DeleteExpenseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteExpenseTool: ToolConfig = { + id: 'sap_concur_delete_expense', + name: 'SAP Concur Delete Expense', + description: + 'Delete an expense (DELETE /expensereports/v4/reports/{reportId}/expenses/{expenseId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/delete_expense_report.ts b/apps/sim/tools/sap_concur/delete_expense_report.ts new file mode 100644 index 00000000000..963dbcd6a86 --- /dev/null +++ b/apps/sim/tools/sap_concur/delete_expense_report.ts @@ -0,0 +1,86 @@ +import type { DeleteExpenseReportParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteExpenseReportTool: ToolConfig< + DeleteExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_delete_expense_report', + name: 'SAP Concur Delete Expense Report', + description: 'Delete an expense report (DELETE /expensereports/v4/reports/{reportId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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)' }, + }, +} diff --git a/apps/sim/tools/sap_concur/delete_list_item.ts b/apps/sim/tools/sap_concur/delete_list_item.ts new file mode 100644 index 00000000000..da9c4d1611f --- /dev/null +++ b/apps/sim/tools/sap_concur/delete_list_item.ts @@ -0,0 +1,88 @@ +import type { DeleteListItemParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteListItemTool: ToolConfig = { + id: 'sap_concur_delete_list_item', + name: 'SAP Concur Delete List Item', + description: 'Delete a list item (DELETE /list/v4/items/{itemId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/delete_travel_request.ts b/apps/sim/tools/sap_concur/delete_travel_request.ts new file mode 100644 index 00000000000..6b04c0e1479 --- /dev/null +++ b/apps/sim/tools/sap_concur/delete_travel_request.ts @@ -0,0 +1,99 @@ +import type { DeleteTravelRequestParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteTravelRequestTool: ToolConfig< + DeleteTravelRequestParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_delete_travel_request', + name: 'SAP Concur Delete Travel Request', + description: 'Delete a travel request (DELETE /travelrequest/v4/requests/{requestUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 = {} + if (params.userId) query.userId = params.userId + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests/${encodeURIComponent(requestUuid)}`, + method: 'DELETE', + query: Object.keys(query).length > 0 ? query : undefined, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Concur delete response payload (boolean true on 200 OK)', + properties: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/delete_user.ts b/apps/sim/tools/sap_concur/delete_user.ts new file mode 100644 index 00000000000..d153001874a --- /dev/null +++ b/apps/sim/tools/sap_concur/delete_user.ts @@ -0,0 +1,87 @@ +import type { DeleteUserParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteUserTool: ToolConfig = { + id: 'sap_concur_delete_user', + name: 'SAP Concur Delete User', + description: 'Delete a user identity (DELETE /profile/identity/v4.1/Users/{id}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_allocation.ts b/apps/sim/tools/sap_concur/get_allocation.ts new file mode 100644 index 00000000000..21647a35026 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_allocation.ts @@ -0,0 +1,156 @@ +import type { GetAllocationParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getAllocationTool: ToolConfig = { + id: 'sap_concur_get_allocation', + name: 'SAP Concur Get Allocation', + description: + 'Get a single allocation (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/allocations/{allocationId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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', + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_budget.ts b/apps/sim/tools/sap_concur/get_budget.ts new file mode 100644 index 00000000000..fbefaef6e3f --- /dev/null +++ b/apps/sim/tools/sap_concur/get_budget.ts @@ -0,0 +1,179 @@ +import type { GetBudgetParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getBudgetTool: ToolConfig = { + id: 'sap_concur_get_budget', + name: 'SAP Concur Get Budget', + description: 'Get a budget item header by ID (GET /budget/v4/budgetItemHeader/{id}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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)', + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_cash_advance.ts b/apps/sim/tools/sap_concur/get_cash_advance.ts new file mode 100644 index 00000000000..ace1a6315f1 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_cash_advance.ts @@ -0,0 +1,191 @@ +import type { GetCashAdvanceParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getCashAdvanceTool: ToolConfig = { + id: 'sap_concur_get_cash_advance', + name: 'SAP Concur Get Cash Advance', + description: 'Get a cash advance (GET /cashadvance/v4.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, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_expected_expense.ts b/apps/sim/tools/sap_concur/get_expected_expense.ts new file mode 100644 index 00000000000..ae830bb51a6 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_expected_expense.ts @@ -0,0 +1,167 @@ +import type { GetExpectedExpenseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getExpectedExpenseTool: ToolConfig = + { + id: 'sap_concur_get_expected_expense', + name: 'SAP Concur Get Expected Expense', + description: 'Get an expected expense (GET /travelrequest/v4/expenses/{expenseUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 = {} + if (params.userId?.trim()) query.userId = params.userId.trim() + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/expenses/${encodeURIComponent(expenseUuid)}`, + method: 'GET', + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Expected expense payload', + properties: { + id: { type: 'string', description: 'Expected expense identifier', optional: true }, + href: { type: 'string', description: 'Self-link', optional: true }, + expenseType: { + type: 'json', + description: 'Expense type {id, name}', + optional: true, + }, + transactionDate: { + type: 'string', + description: 'Transaction date', + optional: true, + }, + transactionAmount: { + type: 'json', + description: 'Transaction amount {value, currencyCode}', + optional: true, + }, + postedAmount: { + type: 'json', + description: 'Posted amount {value, currencyCode}', + optional: true, + }, + approvedAmount: { + type: 'json', + description: 'Approved amount {value, currencyCode}', + optional: true, + }, + remainingAmount: { + type: 'json', + description: 'Remaining amount on the expected expense', + optional: true, + }, + businessPurpose: { + type: 'string', + description: 'Business purpose of the expense', + optional: true, + }, + location: { + type: 'json', + description: + 'Location {id, name, city, countryCode, countrySubDivisionCode, iataCode, locationType}', + optional: true, + }, + exchangeRate: { + type: 'json', + description: 'Exchange rate {value, operation}', + optional: true, + }, + allocations: { + type: 'json', + description: 'Budget allocations array', + optional: true, + }, + tripData: { + type: 'json', + description: + 'Trip data {agencyBooked, selfBooked, tripType (ONE_WAY|ROUND_TRIP), legs[{id, returnLeg, startDate, startTime, startLocationDetail, startLocation, endLocation, class {code,value}, travelExceptionReasonCodes}], segmentType {category, code}}', + optional: true, + }, + parentRequest: { + type: 'json', + description: 'Parent travel request resource link {href, id}', + optional: true, + }, + comments: { + type: 'json', + description: 'Comments sub-resource link {href, id}', + optional: true, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/get_expense.ts b/apps/sim/tools/sap_concur/get_expense.ts new file mode 100644 index 00000000000..db7bf2e11ee --- /dev/null +++ b/apps/sim/tools/sap_concur/get_expense.ts @@ -0,0 +1,357 @@ +import type { GetExpenseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getExpenseTool: ToolConfig = { + id: 'sap_concur_get_expense', + name: 'SAP Concur Get Expense', + description: + 'Get a single expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_expense_report.ts b/apps/sim/tools/sap_concur/get_expense_report.ts new file mode 100644 index 00000000000..c677513bd34 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_expense_report.ts @@ -0,0 +1,292 @@ +import type { GetExpenseReportParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getExpenseReportTool: ToolConfig = { + id: 'sap_concur_get_expense_report', + name: 'SAP Concur Get Expense Report', + description: + 'Retrieve a single expense report header by id via Expense Report v4 (/expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_itemizations.ts b/apps/sim/tools/sap_concur/get_itemizations.ts new file mode 100644 index 00000000000..f8645c71c3a --- /dev/null +++ b/apps/sim/tools/sap_concur/get_itemizations.ts @@ -0,0 +1,174 @@ +import type { GetItemizationsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getItemizationsTool: ToolConfig = { + id: 'sap_concur_get_itemizations', + name: 'SAP Concur Get Expense Itemizations', + description: + 'Get expense itemizations (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/itemizations).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_itinerary.ts b/apps/sim/tools/sap_concur/get_itinerary.ts new file mode 100644 index 00000000000..bf6521d3c96 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_itinerary.ts @@ -0,0 +1,230 @@ +import type { GetItineraryParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getItineraryTool: ToolConfig = { + id: 'sap_concur_get_itinerary', + name: 'SAP Concur Get Trip', + description: 'Get a single trip/itinerary (GET /api/travel/trip/v1.1/{tripID}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_list.ts b/apps/sim/tools/sap_concur/get_list.ts new file mode 100644 index 00000000000..5bd7229577e --- /dev/null +++ b/apps/sim/tools/sap_concur/get_list.ts @@ -0,0 +1,134 @@ +import type { GetListParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getListTool: ToolConfig = { + id: 'sap_concur_get_list', + name: 'SAP Concur Get List', + description: 'Get a single custom list (GET /list/v4/lists/{listId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_list_item.ts b/apps/sim/tools/sap_concur/get_list_item.ts new file mode 100644 index 00000000000..80ed6af505c --- /dev/null +++ b/apps/sim/tools/sap_concur/get_list_item.ts @@ -0,0 +1,123 @@ +import type { GetListItemParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getListItemTool: ToolConfig = { + id: 'sap_concur_get_list_item', + name: 'SAP Concur Get List Item', + description: 'Get a single list item (GET /list/v4/items/{itemId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_purchase_request.ts b/apps/sim/tools/sap_concur/get_purchase_request.ts new file mode 100644 index 00000000000..b497ac5369c --- /dev/null +++ b/apps/sim/tools/sap_concur/get_purchase_request.ts @@ -0,0 +1,155 @@ +import type { GetPurchaseRequestParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getPurchaseRequestTool: ToolConfig = + { + id: 'sap_concur_get_purchase_request', + name: 'SAP Concur Get Purchase Request', + description: 'Get a purchase request by ID (GET /purchaserequest/v4/purchaserequests/{id}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/get_receipt.ts b/apps/sim/tools/sap_concur/get_receipt.ts new file mode 100644 index 00000000000..fa3fe9c9088 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_receipt.ts @@ -0,0 +1,120 @@ +import type { GetReceiptParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getReceiptTool: ToolConfig = { + id: 'sap_concur_get_receipt', + name: 'SAP Concur Get Receipt', + description: 'Get a single receipt by ID (GET /receipts/v4/{receiptId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_receipt_status.ts b/apps/sim/tools/sap_concur/get_receipt_status.ts new file mode 100644 index 00000000000..bcd9abbe557 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_receipt_status.ts @@ -0,0 +1,106 @@ +import type { GetReceiptStatusParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getReceiptStatusTool: ToolConfig = { + id: 'sap_concur_get_receipt_status', + name: 'SAP Concur Get Receipt Status', + description: 'Get receipt processing status (GET /receipts/v4/status/{receiptId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_request_cash_advance.ts b/apps/sim/tools/sap_concur/get_request_cash_advance.ts new file mode 100644 index 00000000000..c013e3f1bfd --- /dev/null +++ b/apps/sim/tools/sap_concur/get_request_cash_advance.ts @@ -0,0 +1,130 @@ +import type { GetRequestCashAdvanceParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getRequestCashAdvanceTool: ToolConfig< + GetRequestCashAdvanceParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_get_request_cash_advance', + name: 'SAP Concur Get Request Cash Advance', + description: + 'Get a single cash advance assigned to a travel request (GET /travelrequest/v4/cashadvances/{cashAdvanceUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_travel_profile.ts b/apps/sim/tools/sap_concur/get_travel_profile.ts new file mode 100644 index 00000000000..52d98ee2439 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_travel_profile.ts @@ -0,0 +1,234 @@ +import type { GetTravelProfileParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getTravelProfileTool: ToolConfig = { + id: 'sap_concur_get_travel_profile', + name: 'SAP Concur Get Travel Profile', + description: + 'Get a travel profile (GET /api/travelprofile/v2.0/profile). Returns the calling user by default; pass userid_type and userid_value to impersonate.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_travel_request.ts b/apps/sim/tools/sap_concur/get_travel_request.ts new file mode 100644 index 00000000000..4f6da5fbcd3 --- /dev/null +++ b/apps/sim/tools/sap_concur/get_travel_request.ts @@ -0,0 +1,336 @@ +import type { GetTravelRequestParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getTravelRequestTool: ToolConfig = { + id: 'sap_concur_get_travel_request', + name: 'SAP Concur Get Travel Request', + description: 'Get a single travel request (GET /travelrequest/v4/requests/{requestUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 = {} + if (params.userId) query.userId = params.userId + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests/${encodeURIComponent(requestUuid)}`, + method: 'GET', + query: Object.keys(query).length > 0 ? query : undefined, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Travel request detail payload', + properties: { + id: { type: 'string', description: 'Travel request UUID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + requestId: { + type: 'string', + description: 'Public-facing request ID (4-6 alphanumeric characters)', + optional: true, + }, + name: { type: 'string', description: 'Request name', optional: true }, + businessPurpose: { type: 'string', description: 'Business purpose', optional: true }, + comment: { type: 'string', description: 'Last attached comment', optional: true }, + creationDate: { type: 'string', description: 'Creation timestamp', optional: true }, + lastModified: { + type: 'string', + description: 'Last modification timestamp', + optional: true, + }, + submitDate: { type: 'string', description: 'Last submission timestamp', optional: true }, + authorizedDate: { + type: 'string', + description: 'Date when approval was completed', + optional: true, + }, + approvalLimitDate: { + type: 'string', + description: 'Required approval deadline', + optional: true, + }, + startDate: { type: 'string', description: 'Trip start date (ISO 8601)', optional: true }, + endDate: { type: 'string', description: 'Trip end date (ISO 8601)', optional: true }, + startTime: { type: 'string', description: 'Trip start time (HH:mm)', optional: true }, + endTime: { type: 'string', description: 'Trip end time (HH:mm)', optional: true }, + pnr: { type: 'string', description: 'Passenger record number', optional: true }, + approved: { + type: 'boolean', + description: 'Whether the request is approved', + optional: true, + }, + pendingApproval: { type: 'boolean', description: 'Pending approval flag', optional: true }, + closed: { type: 'boolean', description: 'Closed flag', optional: true }, + everSentBack: { type: 'boolean', description: 'Ever-sent-back flag', optional: true }, + canceledPostApproval: { + type: 'boolean', + description: 'Canceled after approval flag', + optional: true, + }, + isParentRequest: { type: 'boolean', description: 'Parent request flag', optional: true }, + parentRequestId: { + type: 'string', + description: 'Parent budget request ID', + optional: true, + }, + allocationFormId: { + type: 'string', + description: 'Allocation form identifier', + optional: true, + }, + highestExceptionLevel: { + type: 'string', + description: 'Highest exception level (WARNING, ERROR, NONE)', + optional: true, + }, + approvalStatus: { + type: 'json', + description: 'Approval status', + optional: true, + properties: { + code: { + type: 'string', + description: 'Status code (NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK)', + optional: true, + }, + name: { type: 'string', description: 'Localized status name', optional: true }, + }, + }, + owner: { + type: 'json', + description: 'Travel request owner', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { type: 'string', description: 'Owner first name', optional: true }, + lastName: { type: 'string', description: 'Owner last name', optional: true }, + }, + }, + approver: { + type: 'json', + description: 'Approver assigned to the request', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { type: 'string', description: 'Approver first name', optional: true }, + lastName: { type: 'string', description: 'Approver last name', optional: true }, + }, + }, + policy: { + type: 'json', + description: 'Resource link to the applicable policy', + optional: true, + properties: { + id: { type: 'string', description: 'Policy ID', optional: true }, + href: { type: 'string', description: 'Policy hyperlink', optional: true }, + }, + }, + type: { + type: 'json', + description: 'Request type', + optional: true, + properties: { + code: { type: 'string', description: 'Request type code', optional: true }, + label: { type: 'string', description: 'Request type label', optional: true }, + }, + }, + mainDestination: { + type: 'json', + description: 'Main destination of the trip', + optional: true, + properties: { + city: { type: 'string', description: 'City', optional: true }, + countryCode: { type: 'string', description: 'ISO country code', optional: true }, + countrySubDivisionCode: { + type: 'string', + description: 'ISO country sub-division code', + optional: true, + }, + name: { type: 'string', description: 'Destination name', optional: true }, + }, + }, + totalApprovedAmount: { + type: 'json', + description: 'Total approved amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + totalPostedAmount: { + type: 'json', + description: 'Total posted amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + totalRemainingAmount: { + type: 'json', + description: 'Total remaining amount', + optional: true, + properties: { + value: { type: 'number', description: 'Amount value', optional: true }, + currency: { type: 'string', description: 'Currency code', optional: true }, + }, + }, + expenses: { + type: 'array', + description: 'Resource links to expected expenses', + optional: true, + items: { type: 'json' }, + }, + cashAdvances: { + type: 'json', + description: 'Resource link to cash advances', + optional: true, + properties: { + id: { type: 'string', description: 'Resource ID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + }, + }, + comments: { + type: 'json', + description: 'Resource link to comments', + optional: true, + properties: { + id: { type: 'string', description: 'Resource ID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + }, + }, + exceptions: { + type: 'json', + description: 'Resource link to exceptions', + optional: true, + properties: { + id: { type: 'string', description: 'Resource ID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + }, + }, + travelAgency: { + type: 'json', + description: 'Resource link to travel agency', + optional: true, + properties: { + id: { type: 'string', description: 'Resource ID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + }, + }, + parentRequest: { + type: 'json', + description: 'Resource link to parent request', + optional: true, + properties: { + id: { type: 'string', description: 'Resource ID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + }, + }, + eventRequest: { + type: 'json', + description: 'Resource link to parent event request', + optional: true, + properties: { + id: { type: 'string', description: 'Resource ID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + }, + }, + operations: { + type: 'array', + description: 'Available workflow actions', + optional: true, + items: { + type: 'json', + properties: { + rel: { type: 'string', description: 'Operation name', optional: true }, + href: { type: 'string', description: 'Operation URL', optional: true }, + }, + }, + }, + expensePolicy: { + type: 'json', + description: 'Expense policy reference', + optional: true, + properties: { + id: { type: 'string', description: 'Policy identifier', optional: true }, + href: { type: 'string', description: 'Policy URL', optional: true }, + }, + }, + custom1: { type: 'json', description: 'Custom field 1', optional: true }, + custom2: { type: 'json', description: 'Custom field 2', optional: true }, + custom3: { type: 'json', description: 'Custom field 3', optional: true }, + custom4: { type: 'json', description: 'Custom field 4', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/get_user.ts b/apps/sim/tools/sap_concur/get_user.ts new file mode 100644 index 00000000000..f8d12a8e9bf --- /dev/null +++ b/apps/sim/tools/sap_concur/get_user.ts @@ -0,0 +1,105 @@ +import type { GetUserParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + scimUserOutputProperties, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const getUserTool: ToolConfig = { + id: 'sap_concur_get_user', + name: 'SAP Concur Get User', + description: 'Get a single user by UUID (GET /profile/identity/v4.1/Users/{id}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 = {} + if (params.attributes?.trim()) query.attributes = params.attributes.trim() + if (params.excludedAttributes?.trim()) + query.excludedAttributes = params.excludedAttributes.trim() + return { + ...baseProxyBody(params), + path: `/profile/identity/v4.1/Users/${encodeURIComponent(userUuid)}`, + method: 'GET', + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'SCIM User identity payload', + properties: scimUserOutputProperties, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/index.ts b/apps/sim/tools/sap_concur/index.ts new file mode 100644 index 00000000000..65b26376efa --- /dev/null +++ b/apps/sim/tools/sap_concur/index.ts @@ -0,0 +1,70 @@ +export { approveExpenseReportTool } from '@/tools/sap_concur/approve_expense_report' +export { associateAttendeesTool } from '@/tools/sap_concur/associate_attendees' +export { createCashAdvanceTool } from '@/tools/sap_concur/create_cash_advance' +export { createExpectedExpenseTool } from '@/tools/sap_concur/create_expected_expense' +export { createExpenseReportTool } from '@/tools/sap_concur/create_expense_report' +export { createListItemTool } from '@/tools/sap_concur/create_list_item' +export { createPurchaseRequestTool } from '@/tools/sap_concur/create_purchase_request' +export { createQuickExpenseTool } from '@/tools/sap_concur/create_quick_expense' +export { createQuickExpenseWithImageTool } from '@/tools/sap_concur/create_quick_expense_with_image' +export { createReportCommentTool } from '@/tools/sap_concur/create_report_comment' +export { createTravelRequestTool } from '@/tools/sap_concur/create_travel_request' +export { createUserTool } from '@/tools/sap_concur/create_user' +export { deleteExpectedExpenseTool } from '@/tools/sap_concur/delete_expected_expense' +export { deleteExpenseTool } from '@/tools/sap_concur/delete_expense' +export { deleteExpenseReportTool } from '@/tools/sap_concur/delete_expense_report' +export { deleteListItemTool } from '@/tools/sap_concur/delete_list_item' +export { deleteTravelRequestTool } from '@/tools/sap_concur/delete_travel_request' +export { deleteUserTool } from '@/tools/sap_concur/delete_user' +export { getAllocationTool } from '@/tools/sap_concur/get_allocation' +export { getBudgetTool } from '@/tools/sap_concur/get_budget' +export { getCashAdvanceTool } from '@/tools/sap_concur/get_cash_advance' +export { 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' diff --git a/apps/sim/tools/sap_concur/issue_cash_advance.ts b/apps/sim/tools/sap_concur/issue_cash_advance.ts new file mode 100644 index 00000000000..c8e810b8470 --- /dev/null +++ b/apps/sim/tools/sap_concur/issue_cash_advance.ts @@ -0,0 +1,109 @@ +import type { IssueCashAdvanceParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const issueCashAdvanceTool: ToolConfig = { + id: 'sap_concur_issue_cash_advance', + name: 'SAP Concur Issue Cash Advance', + description: 'Issue a cash advance (POST /cashadvance/v4.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 }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_allocations.ts b/apps/sim/tools/sap_concur/list_allocations.ts new file mode 100644 index 00000000000..e0b936a15a2 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_allocations.ts @@ -0,0 +1,117 @@ +import type { ListAllocationsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listAllocationsTool: ToolConfig = { + id: 'sap_concur_list_allocations', + name: 'SAP Concur List Allocations', + description: + 'List allocations on an expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/allocations).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_attendee_associations.ts b/apps/sim/tools/sap_concur/list_attendee_associations.ts new file mode 100644 index 00000000000..6a5e66c38b3 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_attendee_associations.ts @@ -0,0 +1,193 @@ +import type { + ListAttendeeAssociationsParams, + SapConcurProxyResponse, +} from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listAttendeeAssociationsTool: ToolConfig< + ListAttendeeAssociationsParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_list_attendee_associations', + name: 'SAP Concur List Attendee Associations', + description: + 'List attendees associated with an expense (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses/{expenseId}/attendees).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_budget_categories.ts b/apps/sim/tools/sap_concur/list_budget_categories.ts new file mode 100644 index 00000000000..e37005826d0 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_budget_categories.ts @@ -0,0 +1,106 @@ +import type { ListBudgetCategoriesParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listBudgetCategoriesTool: ToolConfig< + ListBudgetCategoriesParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_list_budget_categories', + name: 'SAP Concur List Budget Categories', + description: 'List budget categories (GET /budget/v4/budgetCategory).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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' }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_budgets.ts b/apps/sim/tools/sap_concur/list_budgets.ts new file mode 100644 index 00000000000..4e2d494194f --- /dev/null +++ b/apps/sim/tools/sap_concur/list_budgets.ts @@ -0,0 +1,112 @@ +import type { ListBudgetsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listBudgetsTool: ToolConfig = { + id: 'sap_concur_list_budgets', + name: 'SAP Concur List Budgets', + description: 'List budget item headers (GET /budget/v4/budgetItemHeader).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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' }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_exceptions.ts b/apps/sim/tools/sap_concur/list_exceptions.ts new file mode 100644 index 00000000000..18d9933fc1d --- /dev/null +++ b/apps/sim/tools/sap_concur/list_exceptions.ts @@ -0,0 +1,131 @@ +import type { ListExceptionsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listExceptionsTool: ToolConfig = { + id: 'sap_concur_list_exceptions', + name: 'SAP Concur List Report Exceptions', + description: + 'List exceptions on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/exceptions).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_expected_expenses.ts b/apps/sim/tools/sap_concur/list_expected_expenses.ts new file mode 100644 index 00000000000..8709ae3b422 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_expected_expenses.ts @@ -0,0 +1,100 @@ +import type { ListExpectedExpensesParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listExpectedExpensesTool: ToolConfig< + ListExpectedExpensesParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_list_expected_expenses', + name: 'SAP Concur List Expected Expenses', + description: + 'List expected expenses on a travel request (GET /travelrequest/v4/requests/{requestUuid}/expenses).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 = {} + if (params.userId?.trim()) query.userId = params.userId.trim() + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests/${encodeURIComponent(requestUuid)}/expenses`, + method: 'GET', + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: + 'Array of expected expense objects. Each entry includes id, href, expenseType {id,name}, transactionDate, transactionAmount, postedAmount, approvedAmount, remainingAmount, businessPurpose, location, exchangeRate, allocations, tripData, parentRequest {href, id}, comments {href, id}.', + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_expense_reports.ts b/apps/sim/tools/sap_concur/list_expense_reports.ts new file mode 100644 index 00000000000..b2293bf1eaf --- /dev/null +++ b/apps/sim/tools/sap_concur/list_expense_reports.ts @@ -0,0 +1,275 @@ +import type { ListExpenseReportsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listExpenseReportsTool: ToolConfig = + { + id: 'sap_concur_list_expense_reports', + name: 'SAP Concur List Expense Reports', + description: + 'List expense reports (GET /api/v3.0/expense/reports). Returns a v3 envelope 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 = {} + if (params.user) query.user = params.user + if (params.submitDateBefore) query.submitDateBefore = params.submitDateBefore + if (params.submitDateAfter) query.submitDateAfter = params.submitDateAfter + if (params.paidDateBefore) query.paidDateBefore = params.paidDateBefore + if (params.paidDateAfter) query.paidDateAfter = params.paidDateAfter + if (params.modifiedDateBefore) query.modifiedDateBefore = params.modifiedDateBefore + if (params.modifiedDateAfter) query.modifiedDateAfter = params.modifiedDateAfter + if (params.createDateBefore) query.createDateBefore = params.createDateBefore + if (params.createDateAfter) query.createDateAfter = params.createDateAfter + if (params.approvalStatusCode) query.approvalStatusCode = params.approvalStatusCode + if (params.paymentStatusCode) query.paymentStatusCode = params.paymentStatusCode + if (params.currencyCode) query.currencyCode = params.currencyCode + if (params.approverLoginID) query.approverLoginID = params.approverLoginID + if (params.limit !== undefined) query.limit = params.limit + if (params.offset) query.offset = params.offset + return { + ...baseProxyBody(params), + path: '/api/v3.0/expense/reports', + method: 'GET', + query, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Concur v3 expense reports envelope', + properties: { + Items: { + type: 'array', + description: 'Array of report header objects', + optional: true, + items: { + type: 'json', + properties: { + ID: { type: 'string', description: 'Report ID', optional: true }, + Name: { type: 'string', description: 'Report name', optional: true }, + OwnerLoginID: { type: 'string', description: 'Owner login ID', optional: true }, + OwnerName: { type: 'string', description: 'Owner display name', optional: true }, + Total: { type: 'number', description: 'Report total', optional: true }, + TotalApprovedAmount: { + type: 'number', + description: 'Total approved amount', + optional: true, + }, + TotalClaimedAmount: { + type: 'number', + description: 'Total claimed amount', + optional: true, + }, + AmountDueEmployee: { + type: 'number', + description: 'Amount due employee', + optional: true, + }, + CurrencyCode: { type: 'string', description: 'ISO currency code', optional: true }, + ApprovalStatusName: { + type: 'string', + description: 'Approval status name', + optional: true, + }, + ApprovalStatusCode: { + type: 'string', + description: 'Approval status code', + optional: true, + }, + PaymentStatusName: { + type: 'string', + description: 'Payment status name', + optional: true, + }, + PaymentStatusCode: { + type: 'string', + description: 'Payment status code', + optional: true, + }, + ApproverLoginID: { + type: 'string', + description: 'Approver login ID', + optional: true, + }, + ApproverName: { + type: 'string', + description: 'Approver display name', + optional: true, + }, + HasException: { + type: 'boolean', + description: 'Whether the report has any exception', + optional: true, + }, + ReceiptsReceived: { + type: 'boolean', + description: 'Whether paper receipts were received', + optional: true, + }, + CreateDate: { type: 'string', description: 'Creation date', optional: true }, + SubmitDate: { type: 'string', description: 'Submit date', optional: true }, + LastModifiedDate: { + type: 'string', + description: 'Last modified date', + optional: true, + }, + PaidDate: { type: 'string', description: 'Paid date', optional: true }, + URI: { type: 'string', description: 'Self URI', optional: true }, + }, + }, + }, + NextPage: { + type: 'string', + description: 'URI of the next page (use as offset cursor)', + optional: true, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/list_expenses.ts b/apps/sim/tools/sap_concur/list_expenses.ts new file mode 100644 index 00000000000..8c90cd77495 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_expenses.ts @@ -0,0 +1,216 @@ +import type { ListExpensesParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listExpensesTool: ToolConfig = { + id: 'sap_concur_list_expenses', + name: 'SAP Concur List Expenses', + description: + 'List expenses on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/expenses).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_itineraries.ts b/apps/sim/tools/sap_concur/list_itineraries.ts new file mode 100644 index 00000000000..15e4effcee6 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_itineraries.ts @@ -0,0 +1,232 @@ +import type { ListItinerariesParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listItinerariesTool: ToolConfig = { + id: 'sap_concur_list_itineraries', + name: 'SAP Concur List Trips', + description: 'List travel trips/itineraries (GET /api/travel/trip/v1.1).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_list_items.ts b/apps/sim/tools/sap_concur/list_list_items.ts new file mode 100644 index 00000000000..2f1ea8c14d3 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_list_items.ts @@ -0,0 +1,220 @@ +import type { ListListItemsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listListItemsTool: ToolConfig = { + id: 'sap_concur_list_list_items', + name: 'SAP Concur List List Items', + description: + 'List the top-level items (children) for a custom list (GET /list/v4/lists/{listId}/children).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_lists.ts b/apps/sim/tools/sap_concur/list_lists.ts new file mode 100644 index 00000000000..89c75aaa4ab --- /dev/null +++ b/apps/sim/tools/sap_concur/list_lists.ts @@ -0,0 +1,209 @@ +import type { ListListsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listListsTool: ToolConfig = { + id: 'sap_concur_list_lists', + name: 'SAP Concur List Lists', + description: 'List custom lists (GET /list/v4/lists).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_receipts.ts b/apps/sim/tools/sap_concur/list_receipts.ts new file mode 100644 index 00000000000..c33d6c896b1 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_receipts.ts @@ -0,0 +1,107 @@ +import type { ListReceiptsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listReceiptsTool: ToolConfig = { + id: 'sap_concur_list_receipts', + name: 'SAP Concur List Receipts', + description: 'List receipts for a user (GET /receipts/v4/users/{userId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_report_comments.ts b/apps/sim/tools/sap_concur/list_report_comments.ts new file mode 100644 index 00000000000..f585eae336b --- /dev/null +++ b/apps/sim/tools/sap_concur/list_report_comments.ts @@ -0,0 +1,154 @@ +import type { ListReportCommentsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listReportCommentsTool: ToolConfig = + { + id: 'sap_concur_list_report_comments', + name: 'SAP Concur List Report Comments', + description: + 'List comments on a report (GET /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/comments).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/list_reports_to_approve.ts b/apps/sim/tools/sap_concur/list_reports_to_approve.ts new file mode 100644 index 00000000000..2317885ac06 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_reports_to_approve.ts @@ -0,0 +1,175 @@ +import type { ListReportsToApproveParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listReportsToApproveTool: ToolConfig< + ListReportsToApproveParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_list_reports_to_approve', + name: 'SAP Concur List Reports To Approve', + description: + 'List expense reports awaiting approval (GET /expensereports/v4/users/{userId}/context/MANAGER/reportsToApprove).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_travel_profiles_summary.ts b/apps/sim/tools/sap_concur/list_travel_profiles_summary.ts new file mode 100644 index 00000000000..c3896f6f899 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_travel_profiles_summary.ts @@ -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 }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_travel_request_comments.ts b/apps/sim/tools/sap_concur/list_travel_request_comments.ts new file mode 100644 index 00000000000..7c2bd5b6f71 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_travel_request_comments.ts @@ -0,0 +1,118 @@ +import type { + ListTravelRequestCommentsParams, + SapConcurProxyResponse, +} from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listTravelRequestCommentsTool: ToolConfig< + ListTravelRequestCommentsParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_list_travel_request_comments', + name: 'SAP Concur List Travel Request Comments', + description: + 'List comments on a travel request (GET /travelrequest/v4/requests/{requestUuid}/comments).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/list_travel_requests.ts b/apps/sim/tools/sap_concur/list_travel_requests.ts new file mode 100644 index 00000000000..eca2cfcd6ac --- /dev/null +++ b/apps/sim/tools/sap_concur/list_travel_requests.ts @@ -0,0 +1,349 @@ +import type { ListTravelRequestsParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listTravelRequestsTool: ToolConfig = + { + id: 'sap_concur_list_travel_requests', + name: 'SAP Concur List Travel Requests', + description: 'List travel requests (GET /travelrequest/v4/requests).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 }, + }, + }, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/list_users.ts b/apps/sim/tools/sap_concur/list_users.ts new file mode 100644 index 00000000000..8fe92c17981 --- /dev/null +++ b/apps/sim/tools/sap_concur/list_users.ts @@ -0,0 +1,109 @@ +import type { ListUsersParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + buildListQuery, + SAP_CONCUR_PROXY_URL, + scimListResponseOutputProperties, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const listUsersTool: ToolConfig = { + id: 'sap_concur_list_users', + name: 'SAP Concur List Users', + description: 'List Concur user identities (GET /profile/identity/v4.1/Users).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/move_travel_request.ts b/apps/sim/tools/sap_concur/move_travel_request.ts new file mode 100644 index 00000000000..346fc7dd7a4 --- /dev/null +++ b/apps/sim/tools/sap_concur/move_travel_request.ts @@ -0,0 +1,151 @@ +import type { MoveTravelRequestParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const moveTravelRequestTool: ToolConfig = { + id: 'sap_concur_move_travel_request', + name: 'SAP Concur Move Travel Request', + description: + 'Move a travel request through workflow (POST /travelrequest/v4/requests/{requestUuid}/{action}). Valid actions: submit, recall, cancel, approve, sendback, close, reopen.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 = {} + if (params.userId) query.userId = params.userId + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/requests/${encodeURIComponent(requestUuid)}/${encodeURIComponent(action)}`, + method: 'POST', + body: params.body ?? {}, + query: Object.keys(query).length > 0 ? query : undefined, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Workflow transition response payload', + properties: { + id: { type: 'string', description: 'Travel request UUID', optional: true }, + href: { type: 'string', description: 'Resource hyperlink', optional: true }, + approvalStatus: { + type: 'json', + description: 'Approval status after the workflow transition', + optional: true, + properties: { + code: { + type: 'string', + description: 'Status code (NOT_SUBMITTED, SUBMITTED, APPROVED, CANCELED, SENTBACK)', + optional: true, + }, + name: { type: 'string', description: 'Localized status name', optional: true }, + }, + }, + approver: { + type: 'json', + description: 'Approver assigned after the transition', + optional: true, + properties: { + id: { type: 'string', description: 'User UUID', optional: true }, + firstName: { type: 'string', description: 'Approver first name', optional: true }, + lastName: { type: 'string', description: 'Approver last name', optional: true }, + }, + }, + operations: { + type: 'array', + description: 'Available follow-up workflow actions', + optional: true, + items: { + type: 'json', + properties: { + rel: { type: 'string', description: 'Link relation', optional: true }, + href: { type: 'string', description: 'Link target', optional: true }, + method: { type: 'string', description: 'HTTP method', optional: true }, + name: { type: 'string', description: 'Link name', optional: true }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/recall_expense_report.ts b/apps/sim/tools/sap_concur/recall_expense_report.ts new file mode 100644 index 00000000000..860b6f2f561 --- /dev/null +++ b/apps/sim/tools/sap_concur/recall_expense_report.ts @@ -0,0 +1,109 @@ +import type { RecallExpenseReportParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const recallExpenseReportTool: ToolConfig< + RecallExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_recall_expense_report', + name: 'SAP Concur Recall Expense Report', + description: + 'Recall a submitted expense report (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/recall — supported contexts: TRAVELER, PROXY). No request body is required.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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)' }, + }, +} diff --git a/apps/sim/tools/sap_concur/remove_all_attendees.ts b/apps/sim/tools/sap_concur/remove_all_attendees.ts new file mode 100644 index 00000000000..25003534b1c --- /dev/null +++ b/apps/sim/tools/sap_concur/remove_all_attendees.ts @@ -0,0 +1,110 @@ +import type { RemoveAllAttendeesParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const removeAllAttendeesTool: ToolConfig = + { + id: 'sap_concur_remove_all_attendees', + name: 'SAP Concur Remove All Attendees', + description: + 'Remove all attendees from an expense (DELETE /expensereports/v4/users/{userId}/context/{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: {}, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/search_locations.ts b/apps/sim/tools/sap_concur/search_locations.ts new file mode 100644 index 00000000000..2f28e7e1816 --- /dev/null +++ b/apps/sim/tools/sap_concur/search_locations.ts @@ -0,0 +1,227 @@ +import type { SapConcurProxyResponse, SearchLocationsParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const searchLocationsTool: ToolConfig = { + id: 'sap_concur_search_locations', + name: 'SAP Concur Search Locations', + description: 'Search Concur location reference data (GET /localities/v5/locations).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 = {} + if (params.searchText) query.searchText = params.searchText + if (params.locCode) query.locCode = params.locCode + if (params.locationNameId) query.locationNameId = params.locationNameId + if (params.locationNameKey !== undefined && params.locationNameKey !== null) + query.locationNameKey = params.locationNameKey + if ( + query.searchText === undefined && + query.locCode === undefined && + query.locationNameId === undefined && + query.locationNameKey === undefined + ) { + throw new Error( + 'search_locations requires at least one of: searchText, locCode, locationNameId, locationNameKey' + ) + } + if (params.countryCode) query.countryCode = params.countryCode + if (params.subdivisionCode) query.subdivisionCode = params.subdivisionCode + if (params.adminRegionId) query.adminRegionId = params.adminRegionId + return { + ...baseProxyBody(params), + path: '/localities/v5/locations', + method: 'GET', + query, + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Localities v5 search response', + properties: { + locations: { + type: 'array', + description: 'Array of matching Location records', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Location ID (UUID)', optional: true }, + code: { type: 'string', description: 'IATA / location code', optional: true }, + legacyKey: { + type: 'number', + description: 'Legacy numeric location key', + optional: true, + }, + timeZoneOffset: { + type: 'string', + description: 'IANA timezone or UTC offset', + optional: true, + }, + active: { + type: 'boolean', + description: 'Whether the location is active', + optional: true, + }, + point: { + type: 'json', + description: 'Geographic coordinates', + optional: true, + properties: { + latitude: { type: 'number', description: 'Latitude', optional: true }, + longitude: { type: 'number', description: 'Longitude', optional: true }, + }, + }, + names: { + type: 'array', + description: 'Localized location names', + optional: true, + items: { + type: 'json', + properties: { + id: { type: 'string', description: 'Name ID', optional: true }, + key: { type: 'number', description: 'Numeric name key', optional: true }, + locale: { type: 'string', description: 'Locale tag', optional: true }, + name: { type: 'string', description: 'Display name', optional: true }, + }, + }, + }, + administrativeRegion: { + type: 'json', + description: 'Administrative region (e.g., metro area)', + optional: true, + properties: { + id: { type: 'string', description: 'Region ID', optional: true }, + name: { type: 'string', description: 'Region name', optional: true }, + }, + }, + country: { + type: 'json', + description: 'Country reference', + optional: true, + properties: { + id: { type: 'string', description: 'Country ID', optional: true }, + code: { type: 'string', description: 'ISO country code', optional: true }, + name: { type: 'string', description: 'Country name', optional: true }, + }, + }, + subDivision: { + type: 'json', + description: 'Country subdivision (state/province)', + optional: true, + properties: { + id: { type: 'string', description: 'Subdivision ID', optional: true }, + code: { type: 'string', description: 'ISO subdivision code', optional: true }, + name: { type: 'string', description: 'Subdivision name', optional: true }, + }, + }, + links: { + type: 'array', + description: 'HATEOAS links', + optional: true, + items: { type: 'json' }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/search_users.ts b/apps/sim/tools/sap_concur/search_users.ts new file mode 100644 index 00000000000..1caacbb2959 --- /dev/null +++ b/apps/sim/tools/sap_concur/search_users.ts @@ -0,0 +1,87 @@ +import type { SapConcurProxyResponse, SearchUsersParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + scimListResponseOutputProperties, + transformSapConcurProxyResponse, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const searchUsersTool: ToolConfig = { + id: 'sap_concur_search_users', + name: 'SAP Concur Search Users', + description: + 'Search users via SCIM .search endpoint (POST /profile/identity/v4.1/Users/.search).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/send_back_expense_report.ts b/apps/sim/tools/sap_concur/send_back_expense_report.ts new file mode 100644 index 00000000000..fb1f0a1917b --- /dev/null +++ b/apps/sim/tools/sap_concur/send_back_expense_report.ts @@ -0,0 +1,95 @@ +import type { SapConcurProxyResponse, SendBackExpenseReportParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const sendBackExpenseReportTool: ToolConfig< + SendBackExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_send_back_expense_report', + name: 'SAP Concur Send Back Expense Report', + description: + 'Send back an expense report to the employee (PATCH /expensereports/v4/reports/{reportId}/sendBack). Required body field: comment.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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)' }, + }, +} diff --git a/apps/sim/tools/sap_concur/submit_expense_report.ts b/apps/sim/tools/sap_concur/submit_expense_report.ts new file mode 100644 index 00000000000..f5df02a159f --- /dev/null +++ b/apps/sim/tools/sap_concur/submit_expense_report.ts @@ -0,0 +1,102 @@ +import type { SapConcurProxyResponse, SubmitExpenseReportParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const submitExpenseReportTool: ToolConfig< + SubmitExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_submit_expense_report', + name: 'SAP Concur Submit Expense Report', + description: + 'Submit an expense report into the workflow via Expense Report v4 (PATCH /expensereports/v4/users/{userId}/reports/{reportId}/submit).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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)' }, + }, +} diff --git a/apps/sim/tools/sap_concur/types.ts b/apps/sim/tools/sap_concur/types.ts new file mode 100644 index 00000000000..86980bef61a --- /dev/null +++ b/apps/sim/tools/sap_concur/types.ts @@ -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 +} + +export interface UpdateExpenseReportParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + reportId: string + body: Record | string +} + +export interface DeleteExpenseReportParams extends SapConcurBaseParams { + reportId: string +} + +export interface SubmitExpenseReportParams extends SapConcurBaseParams { + userId: string + reportId: string + body?: Record | string +} + +export interface RecallExpenseReportParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + reportId: string + body?: Record | string +} + +export interface ApproveExpenseReportParams extends SapConcurBaseParams { + reportId: string + body: Record | string +} + +export interface SendBackExpenseReportParams extends SapConcurBaseParams { + reportId: string + body: Record | string +} + +export interface ListExpensesParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' + reportId: string +} + +export interface GetExpenseParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' | 'PROXY' + reportId: string + expenseId: string +} + +export interface GetItemizationsParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' + reportId: string + expenseId: string +} + +export interface UpdateExpenseParams extends SapConcurBaseParams { + reportId: string + expenseId: string + body: Record | string +} + +export interface DeleteExpenseParams extends SapConcurBaseParams { + reportId: string + expenseId: string +} + +export interface ListAllocationsParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' | 'PROXY' + reportId: string + expenseId: string +} + +export interface GetAllocationParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'MANAGER' | 'PROXY' + reportId: string + allocationId: string +} + +export interface UpdateAllocationParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + reportId: string + allocationId: string + body: Record | string +} + +export interface ListAttendeeAssociationsParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + reportId: string + expenseId: string +} + +export interface AssociateAttendeesParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + reportId: string + expenseId: string + body: Record | string +} + +export interface RemoveAllAttendeesParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | 'PROXY' + reportId: string + expenseId: string +} + +export interface ListReportCommentsParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' | '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 +} + +export interface ListReceiptsParams extends SapConcurBaseParams { + userId: string +} + +export interface GetReceiptParams extends SapConcurBaseParams { + receiptId: string +} + +export interface GetReceiptStatusParams extends SapConcurBaseParams { + receiptId: string +} + +export interface ListTravelRequestsParams extends SapConcurBaseParams { + view?: string + limit?: number + start?: number + userId?: string + approvedBefore?: string + approvedAfter?: string + modifiedBefore?: string + modifiedAfter?: string + sortField?: string + sortOrder?: 'asc' | 'desc' +} + +export interface GetTravelRequestParams extends SapConcurBaseParams { + requestUuid: string + userId?: string +} + +export interface CreateTravelRequestParams extends SapConcurBaseParams { + userId?: string + body: Record | string +} + +export interface UpdateTravelRequestParams extends SapConcurBaseParams { + requestUuid: string + body: Record | string +} + +export interface DeleteTravelRequestParams extends SapConcurBaseParams { + requestUuid: string + userId?: string +} + +export interface MoveTravelRequestParams extends SapConcurBaseParams { + requestUuid: string + action: string + userId?: string + body?: Record | string +} + +export interface ListTravelRequestCommentsParams extends SapConcurBaseParams { + requestUuid: string +} + +export interface GetRequestCashAdvanceParams extends SapConcurBaseParams { + cashAdvanceUuid: string +} + +export interface CreateExpectedExpenseParams extends SapConcurBaseParams { + requestUuid: string + userId?: string + body: Record | string +} + +export interface ListExpectedExpensesParams extends SapConcurBaseParams { + requestUuid: string + userId?: string +} + +export interface GetExpectedExpenseParams extends SapConcurBaseParams { + expenseUuid: string + userId?: string +} + +export interface UpdateExpectedExpenseParams extends SapConcurBaseParams { + expenseUuid: string + userId?: string + body: Record | string +} + +export interface DeleteExpectedExpenseParams extends SapConcurBaseParams { + expenseUuid: string + userId?: string +} + +export interface GetCashAdvanceParams extends SapConcurBaseParams { + cashAdvanceId: string +} + +export interface CreateCashAdvanceParams extends SapConcurBaseParams { + body: Record | string +} + +export interface IssueCashAdvanceParams extends SapConcurBaseParams { + cashAdvanceId: string + body?: Record | string +} + +export interface ListItinerariesParams extends SapConcurBaseParams { + startDate?: string + endDate?: string + bookingType?: string + useridType?: string + useridValue?: string + itemsPerPage?: number + page?: number + includeMetadata?: boolean + includeCanceledTrips?: boolean + createdAfterDate?: string + createdBeforeDate?: string + lastModifiedDate?: string +} + +export interface GetItineraryParams extends SapConcurBaseParams { + tripId: string + useridType?: string + useridValue?: string + systemFormat?: string +} + +export interface ListUsersParams extends SapConcurBaseParams { + count?: number + cursor?: string + attributes?: string + excludedAttributes?: string +} + +export interface SearchUsersParams extends SapConcurBaseParams { + body: Record | string +} + +export interface GetUserParams extends SapConcurBaseParams { + userUuid: string + attributes?: string + excludedAttributes?: string +} + +export interface CreateUserParams extends SapConcurBaseParams { + body: Record | string +} + +export interface UpdateUserParams extends SapConcurBaseParams { + userUuid: string + body: Record | string +} + +export interface DeleteUserParams extends SapConcurBaseParams { + userUuid: string +} + +export interface GetTravelProfileParams extends SapConcurBaseParams { + loginId?: string + userId?: string + useridType?: 'login' | 'xmlsyncid' | 'uuid' + useridValue?: string +} + +export interface ListTravelProfilesSummaryParams extends SapConcurBaseParams { + lastModifiedDate: string + page?: number + itemsPerPage?: number + travelConfigs?: string +} + +export interface SearchLocationsParams extends SapConcurBaseParams { + searchText?: string + locCode?: string + locationNameId?: string + locationNameKey?: number + countryCode?: string + subdivisionCode?: string + adminRegionId?: string +} + +export interface CreateListItemParams extends SapConcurBaseParams { + body: Record | string +} + +export interface UpdateListItemParams extends SapConcurBaseParams { + itemId: string + body: Record | string +} + +export interface DeleteListItemParams extends SapConcurBaseParams { + itemId: string +} + +export interface UserFileLike { + id?: string + key?: string + path?: string + url?: string + name: string + size: number + type?: string + [key: string]: unknown +} + +export interface UploadReceiptImageParams extends SapConcurBaseParams { + userId: string + receipt: UserFileLike + forwardId?: string +} + +export interface CreateQuickExpenseWithImageParams extends SapConcurBaseParams { + userId: string + contextType: 'TRAVELER' + receipt: UserFileLike + body: Record | string +} + +export interface ListInvoicesParams extends SapConcurBaseParams { + limit?: number + offset?: number + modifiedAfter?: string +} + +export interface GetInvoiceParams extends SapConcurBaseParams { + invoiceId: string +} + +export interface ListPurchaseOrdersParams extends SapConcurBaseParams { + limit?: number + offset?: number +} + +export interface GetPurchaseOrderParams extends SapConcurBaseParams { + purchaseOrderId: string +} + +export interface ListVendorsParams extends SapConcurBaseParams { + limit?: number + offset?: number + vendorCode?: string +} + +export interface ListPurchaseRequestsParams extends SapConcurBaseParams { + limit?: number + offset?: number + modifiedAfter?: string +} + +export interface GetPurchaseRequestParams extends SapConcurBaseParams { + purchaseRequestId: string +} + +export interface CreatePurchaseRequestParams extends SapConcurBaseParams { + body: Record | string +} + +export interface UpdatePurchaseRequestParams extends SapConcurBaseParams { + purchaseRequestId: string + body: Record | string +} + +export interface ListListsParams extends SapConcurBaseParams { + page?: number + sortBy?: string + sortDirection?: 'asc' | 'desc' + value?: string + categoryType?: string + isDeleted?: boolean + levelCount?: number +} + +export interface GetListParams extends SapConcurBaseParams { + listId: string +} + +export interface ListListItemsParams extends SapConcurBaseParams { + listId: string + page?: number + sortBy?: 'value' | 'shortCode' + sortDirection?: 'asc' | 'desc' + hasChildren?: boolean + isDeleted?: boolean + shortCode?: string + value?: string + shortCodeOrValue?: string +} + +export interface GetListItemParams extends SapConcurBaseParams { + itemId: string +} + +export interface ListBudgetsParams extends SapConcurBaseParams { + adminView?: boolean + offset?: number + responseSchema?: 'COMPACT' +} + +export interface GetBudgetParams extends SapConcurBaseParams { + budgetId: string +} + +export interface ListBudgetItemsParams extends SapConcurBaseParams { + budgetId: string + limit?: number + offset?: number +} + +export type ListBudgetCategoriesParams = SapConcurBaseParams + +export interface ListCardTransactionsParams extends SapConcurBaseParams { + limit?: number + offset?: number + cardAccountId?: string + user?: string + modifiedAfter?: string +} + +export interface GetCardTransactionParams extends SapConcurBaseParams { + cardTransactionId: string +} + +export interface UploadExchangeRatesParams extends SapConcurBaseParams { + body: Record | string +} + +export interface ListLocalitiesParams extends SapConcurBaseParams { + limit?: number + offset?: number + countryCode?: string +} diff --git a/apps/sim/tools/sap_concur/update_allocation.ts b/apps/sim/tools/sap_concur/update_allocation.ts new file mode 100644 index 00000000000..c7759a430b7 --- /dev/null +++ b/apps/sim/tools/sap_concur/update_allocation.ts @@ -0,0 +1,116 @@ +import type { SapConcurProxyResponse, UpdateAllocationParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateAllocationTool: ToolConfig = { + id: 'sap_concur_update_allocation', + name: 'SAP Concur Update Allocation', + description: + 'Update an allocation (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId}/allocations/{allocationId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/update_expected_expense.ts b/apps/sim/tools/sap_concur/update_expected_expense.ts new file mode 100644 index 00000000000..754133e6e7f --- /dev/null +++ b/apps/sim/tools/sap_concur/update_expected_expense.ts @@ -0,0 +1,177 @@ +import type { SapConcurProxyResponse, UpdateExpectedExpenseParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateExpectedExpenseTool: ToolConfig< + UpdateExpectedExpenseParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_update_expected_expense', + name: 'SAP Concur Update Expected Expense', + description: 'Update an expected expense (PUT /travelrequest/v4/expenses/{expenseUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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 = {} + if (params.userId?.trim()) query.userId = params.userId.trim() + return { + ...baseProxyBody(params), + path: `/travelrequest/v4/expenses/${encodeURIComponent(expenseUuid)}`, + method: 'PUT', + body: params.body, + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, + }, + transformResponse: transformSapConcurProxyResponse, + outputs: { + status: { type: 'number', description: 'HTTP status code returned by Concur' }, + data: { + type: 'json', + description: 'Updated expected expense payload', + properties: { + id: { type: 'string', description: 'Expected expense identifier', optional: true }, + href: { type: 'string', description: 'Self-link', optional: true }, + expenseType: { + type: 'json', + description: 'Expense type {id, name}', + optional: true, + }, + transactionDate: { + type: 'string', + description: 'Transaction date', + optional: true, + }, + transactionAmount: { + type: 'json', + description: 'Transaction amount {value, currencyCode}', + optional: true, + }, + postedAmount: { + type: 'json', + description: 'Posted amount {value, currencyCode}', + optional: true, + }, + approvedAmount: { + type: 'json', + description: 'Approved amount {value, currencyCode}', + optional: true, + }, + remainingAmount: { + type: 'json', + description: 'Remaining amount on the expected expense', + optional: true, + }, + businessPurpose: { + type: 'string', + description: 'Business purpose of the expense', + optional: true, + }, + location: { + type: 'json', + description: + 'Location {id, name, city, countryCode, countrySubDivisionCode, iataCode, locationType}', + optional: true, + }, + exchangeRate: { + type: 'json', + description: 'Exchange rate {value, operation}', + optional: true, + }, + allocations: { + type: 'json', + description: 'Budget allocations array', + optional: true, + }, + tripData: { + type: 'json', + description: + 'Trip data {agencyBooked, selfBooked, tripType (ONE_WAY|ROUND_TRIP), legs[{id, returnLeg, startDate, startTime, startLocationDetail, startLocation, endLocation, class {code,value}, travelExceptionReasonCodes}], segmentType {category, code}}', + optional: true, + }, + parentRequest: { + type: 'json', + description: 'Parent travel request resource link {href, id}', + optional: true, + }, + comments: { + type: 'json', + description: 'Comments sub-resource link {href, id}', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/update_expense.ts b/apps/sim/tools/sap_concur/update_expense.ts new file mode 100644 index 00000000000..dcfcbf1c9dd --- /dev/null +++ b/apps/sim/tools/sap_concur/update_expense.ts @@ -0,0 +1,104 @@ +import type { SapConcurProxyResponse, UpdateExpenseParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateExpenseTool: ToolConfig = { + id: 'sap_concur_update_expense', + name: 'SAP Concur Update Expense', + description: + 'Update an expense (PATCH /expensereports/v4/reports/{reportId}/expenses/{expenseId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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: {}, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/update_expense_report.ts b/apps/sim/tools/sap_concur/update_expense_report.ts new file mode 100644 index 00000000000..95f132c4956 --- /dev/null +++ b/apps/sim/tools/sap_concur/update_expense_report.ts @@ -0,0 +1,109 @@ +import type { SapConcurProxyResponse, UpdateExpenseReportParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateExpenseReportTool: ToolConfig< + UpdateExpenseReportParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_update_expense_report', + name: 'SAP Concur Update Expense Report', + description: + 'Update an unsubmitted expense report (PATCH /expensereports/v4/users/{userId}/context/{contextType}/reports/{reportId} — supported contexts: TRAVELER, PROXY). Body fields: businessPurpose, comment, customData, name, etc.', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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)' }, + }, +} diff --git a/apps/sim/tools/sap_concur/update_list_item.ts b/apps/sim/tools/sap_concur/update_list_item.ts new file mode 100644 index 00000000000..b6e4bbb0afa --- /dev/null +++ b/apps/sim/tools/sap_concur/update_list_item.ts @@ -0,0 +1,131 @@ +import type { SapConcurProxyResponse, UpdateListItemParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateListItemTool: ToolConfig = { + id: 'sap_concur_update_list_item', + name: 'SAP Concur Update List Item', + description: 'Update a list item (PUT /list/v4/items/{itemId}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/update_travel_request.ts b/apps/sim/tools/sap_concur/update_travel_request.ts new file mode 100644 index 00000000000..902a7805976 --- /dev/null +++ b/apps/sim/tools/sap_concur/update_travel_request.ts @@ -0,0 +1,231 @@ +import type { SapConcurProxyResponse, UpdateTravelRequestParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateTravelRequestTool: ToolConfig< + UpdateTravelRequestParams, + SapConcurProxyResponse +> = { + id: 'sap_concur_update_travel_request', + name: 'SAP Concur Update Travel Request', + description: 'Update a travel request (PUT /travelrequest/v4/requests/{requestUuid}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/update_user.ts b/apps/sim/tools/sap_concur/update_user.ts new file mode 100644 index 00000000000..baa162556d8 --- /dev/null +++ b/apps/sim/tools/sap_concur/update_user.ts @@ -0,0 +1,95 @@ +import type { SapConcurProxyResponse, UpdateUserParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + SAP_CONCUR_PROXY_URL, + scimUserOutputProperties, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateUserTool: ToolConfig = { + id: 'sap_concur_update_user', + name: 'SAP Concur Update User', + description: 'Patch a user identity (PATCH /profile/identity/v4.1/Users/{id}).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/upload_exchange_rates.ts b/apps/sim/tools/sap_concur/upload_exchange_rates.ts new file mode 100644 index 00000000000..7241da859af --- /dev/null +++ b/apps/sim/tools/sap_concur/upload_exchange_rates.ts @@ -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, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sap_concur/upload_receipt_image.ts b/apps/sim/tools/sap_concur/upload_receipt_image.ts new file mode 100644 index 00000000000..e782e0a73eb --- /dev/null +++ b/apps/sim/tools/sap_concur/upload_receipt_image.ts @@ -0,0 +1,119 @@ +import type { SapConcurProxyResponse, UploadReceiptImageParams } from '@/tools/sap_concur/types' +import { + baseProxyBody, + transformSapConcurProxyResponse, + trimRequired, +} from '@/tools/sap_concur/utils' +import type { ToolConfig } from '@/tools/types' + +export const SAP_CONCUR_UPLOAD_URL = '/api/tools/sap_concur/upload' + +export const uploadReceiptImageTool: ToolConfig = + { + id: 'sap_concur_upload_receipt_image', + name: 'SAP Concur Upload Receipt Image', + description: + 'Upload an image-only receipt (POST /receipts/v4/users/{userId}/image-only-receipts).', + version: '1.0.0', + params: { + datacenter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Concur datacenter base URL (defaults to us.api.concursolutions.com)', + }, + grantType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'OAuth grant type: client_credentials (default) 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, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/sap_concur/utils.ts b/apps/sim/tools/sap_concur/utils.ts new file mode 100644 index 00000000000..c3261a6edda --- /dev/null +++ b/apps/sim/tools/sap_concur/utils.ts @@ -0,0 +1,297 @@ +import type { SapConcurBaseParams, SapConcurProxyResponse } from '@/tools/sap_concur/types' +import type { OutputProperty } from '@/tools/types' + +export const scimUserOutputProperties: Record = { + id: { type: 'string', description: 'User UUID' }, + externalId: { + type: 'string', + description: 'External identifier set by the provisioning client', + optional: true, + }, + userName: { type: 'string', description: 'Unique username (often email)' }, + displayName: { type: 'string', description: 'Display name', optional: true }, + nickName: { type: 'string', description: 'Casual or alternate name', optional: true }, + title: { type: 'string', description: 'Job title', optional: true }, + userType: { type: 'string', description: 'User type (e.g., Employee)', optional: true }, + preferredLanguage: { type: 'string', description: 'Preferred language tag', optional: true }, + locale: { type: 'string', description: 'Locale (e.g., en-US)', optional: true }, + timezone: { type: 'string', description: 'Timezone (e.g., America/Los_Angeles)', optional: true }, + active: { type: 'boolean', description: 'Whether the user is active', optional: true }, + dateOfBirth: { type: 'string', description: 'Date of birth (YYYY-MM-DD)', optional: true }, + name: { + type: 'json', + description: 'Structured name', + optional: true, + properties: { + formatted: { type: 'string', description: 'Formatted full name', optional: true }, + familyName: { type: 'string', description: 'Family (last) name', optional: true }, + familyNamePrefix: { type: 'string', description: 'Family name prefix', optional: true }, + givenName: { type: 'string', description: 'Given (first) name', optional: true }, + middleName: { type: 'string', description: 'Middle name', optional: true }, + honorificPrefix: { type: 'string', description: 'Honorific prefix', optional: true }, + honorificSuffix: { type: 'string', description: 'Honorific suffix', optional: true }, + }, + }, + emails: { + type: 'array', + description: 'Email addresses', + optional: true, + items: { + type: 'json', + properties: { + value: { type: 'string', description: 'Email address' }, + type: { type: 'string', description: 'Type (e.g., work, home)', optional: true }, + primary: { type: 'boolean', description: 'Primary email flag', optional: true }, + display: { type: 'string', description: 'Display label', optional: true }, + notifications: { + type: 'boolean', + description: 'Whether email notifications are enabled', + optional: true, + }, + verified: { type: 'boolean', description: 'Whether the email is verified', optional: true }, + }, + }, + }, + phoneNumbers: { + type: 'array', + description: 'Phone numbers', + optional: true, + items: { + type: 'json', + properties: { + value: { type: 'string', description: 'Phone number' }, + type: { type: 'string', description: 'Type (work, mobile, fax, etc.)', optional: true }, + primary: { type: 'boolean', description: 'Primary phone flag', optional: true }, + display: { type: 'string', description: 'Display label', optional: true }, + notifications: { + type: 'boolean', + description: 'Whether SMS notifications are enabled', + optional: true, + }, + }, + }, + }, + addresses: { + type: 'array', + description: 'Addresses', + optional: true, + items: { + type: 'json', + properties: { + type: { type: 'string', description: 'Address type (work, home, etc.)', optional: true }, + formatted: { type: 'string', description: 'Formatted address', optional: true }, + streetAddress: { type: 'string', description: 'Street address', optional: true }, + locality: { type: 'string', description: 'City / locality', optional: true }, + region: { type: 'string', description: 'State / region', optional: true }, + postalCode: { type: 'string', description: 'Postal code', optional: true }, + country: { type: 'string', description: 'ISO 3166-1 country code', optional: true }, + primary: { type: 'boolean', description: 'Primary address flag', optional: true }, + }, + }, + }, + entitlements: { + type: 'array', + description: 'Entitlements granted to the user', + optional: true, + items: { type: 'json' }, + }, + roles: { + type: 'array', + description: 'Roles assigned to the user', + optional: true, + items: { type: 'json' }, + }, + schemas: { + type: 'array', + description: 'SCIM schemas the resource conforms to', + optional: true, + items: { type: 'string' }, + }, + meta: { + type: 'json', + description: 'Resource metadata', + optional: true, + properties: { + created: { type: 'string', description: 'Creation timestamp', optional: true }, + lastModified: { type: 'string', description: 'Last modified timestamp', optional: true }, + resourceType: { type: 'string', description: 'Resource type (User)', optional: true }, + location: { type: 'string', description: 'Resource URL', optional: true }, + version: { type: 'string', description: 'ETag version', optional: true }, + }, + }, + emergencyContacts: { + type: 'array', + description: 'Emergency contacts', + optional: true, + items: { + type: 'json', + properties: { + name: { type: 'string', description: 'Contact full name', optional: true }, + relationship: { type: 'string', description: 'Relationship to user', optional: true }, + emails: { type: 'array', description: 'Emails', optional: true, items: { type: 'json' } }, + phones: { type: 'array', description: 'Phones', optional: true, items: { type: 'json' } }, + streetAddress: { type: 'string', description: 'Street address', optional: true }, + locality: { type: 'string', description: 'City / locality', optional: true }, + region: { type: 'string', description: 'State / region', optional: true }, + postalCode: { type: 'string', description: 'Postal code', optional: true }, + country: { type: 'string', description: 'ISO 3166-1 country code', optional: true }, + }, + }, + }, + localeOverrides: { + type: 'json', + description: 'Read-only locale and date/time/number preference overrides', + optional: true, + }, + 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User': { + type: 'json', + description: 'SCIM Enterprise User extension', + optional: true, + properties: { + employeeNumber: { type: 'string', description: 'Employee number', optional: true }, + companyId: { type: 'string', description: 'Concur company identifier', optional: true }, + startDate: { type: 'string', description: 'Employment start date', optional: true }, + terminationDate: { + type: 'string', + description: 'Employment termination date', + optional: true, + }, + leavesOfAbsence: { + type: 'array', + description: 'Leaves of absence', + optional: true, + items: { + type: 'json', + properties: { + startDate: { type: 'string', description: 'Leave start date', optional: true }, + endDate: { type: 'string', description: 'Leave end date', optional: true }, + type: { type: 'string', description: 'Leave type', optional: true }, + }, + }, + }, + costCenter: { type: 'string', description: 'Cost center', optional: true }, + organization: { type: 'string', description: 'Organization', optional: true }, + division: { type: 'string', description: 'Division', optional: true }, + department: { type: 'string', description: 'Department', optional: true }, + manager: { + type: 'json', + description: 'Manager reference', + optional: true, + properties: { + value: { type: 'string', description: 'Manager UUID', optional: true }, + $ref: { type: 'string', description: 'Manager resource URL', optional: true }, + displayName: { type: 'string', description: 'Manager display name', optional: true }, + employeeNumber: { + type: 'string', + description: 'Manager employee number', + optional: true, + }, + }, + }, + }, + }, + 'urn:ietf:params:scim:schemas:extension:sap:2.0:User': { + type: 'json', + description: 'SAP SCIM extension', + optional: true, + properties: { + userUuid: { type: 'string', description: 'SAP global user UUID', optional: true }, + }, + }, + 'urn:ietf:params:scim:schemas:extension:sap:concur:2.0:User': { + type: 'json', + description: 'SAP Concur SCIM extension (Concur-specific attributes)', + optional: true, + }, +} + +export const scimListResponseOutputProperties: Record = { + schemas: { + type: 'array', + description: 'SCIM schemas the response conforms to', + optional: true, + items: { type: 'string' }, + }, + totalResults: { + type: 'number', + description: 'Total number of results matching the query', + optional: true, + }, + itemsPerPage: { + type: 'number', + description: 'Number of results returned in this page', + optional: true, + }, + startIndex: { + type: 'number', + description: '1-based index of the first result', + optional: true, + }, + cursor: { + type: 'string', + description: 'SCIM v4.1 cursor for the next page of results', + optional: true, + }, + Resources: { + type: 'array', + description: 'SCIM User resources', + optional: true, + items: { + type: 'json', + properties: scimUserOutputProperties, + }, + }, +} + +export const SAP_CONCUR_PROXY_URL = '/api/tools/sap_concur/proxy' + +export function baseProxyBody(params: SapConcurBaseParams): Record { + const body: Record = { + datacenter: params.datacenter ?? 'us.api.concursolutions.com', + grantType: params.grantType ?? 'client_credentials', + clientId: params.clientId, + clientSecret: params.clientSecret, + } + if (params.username) body.username = params.username + if (params.password) body.password = params.password + if (params.companyUuid) body.companyUuid = params.companyUuid + return body +} + +export function buildListQuery( + params: Record +): Record { + const query: Record = {} + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null) continue + if (typeof value === 'string' && value.trim() === '') continue + query[key] = value + } + return query +} + +export async function transformSapConcurProxyResponse( + response: Response +): Promise { + const data = (await response.json()) as + | { success: true; output: { status: number; data: unknown } } + | { success: false; error?: string; status?: number } + if (!('success' in data) || data.success === false) { + const errMessage = 'error' in data && data.error ? data.error : 'Concur request failed' + throw new Error(errMessage) + } + return { + success: true, + output: { + status: data.output.status, + data: data.output.data, + }, + } +} + +export function trimRequired(value: string | undefined, name: string): string { + if (!value || !value.trim()) { + throw new Error(`${name} is required`) + } + return value.trim() +} diff --git a/apps/sim/tools/sap_s4hana/create_business_partner.ts b/apps/sim/tools/sap_s4hana/create_business_partner.ts index c908a2e118b..09f92ce8d0f 100644 --- a/apps/sim/tools/sap_s4hana/create_business_partner.ts +++ b/apps/sim/tools/sap_s4hana/create_business_partner.ts @@ -17,26 +17,26 @@ export const createBusinessPartnerTool: ToolConfig = params: { subdomain: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)', }, region: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'BTP region (e.g. eu10, us10)', }, clientId: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client ID from the S/4HANA Communication Arrangement', }, clientSecret: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client secret from the S/4HANA Communication Arrangement', }, @@ -111,6 +111,43 @@ export const getCustomerTool: ToolConfig = transformResponse: transformSapProxyResponse, outputs: { status: { type: 'number', description: 'HTTP status code returned by SAP' }, - data: { type: 'json', description: 'A_Customer entity' }, + data: { + type: 'object', + description: 'A_Customer entity', + properties: { + Customer: { type: 'string', description: 'Customer key (up to 10 characters)' }, + CustomerName: { type: 'string', description: 'Name of customer' }, + CustomerFullName: { type: 'string', description: 'Full name of the customer' }, + CustomerAccountGroup: { type: 'string', description: 'Customer account group' }, + CustomerClassification: { type: 'string', description: 'Customer classification code' }, + CustomerCorporateGroup: { type: 'string', description: 'Corporate group code' }, + AuthorizationGroup: { type: 'string', description: 'Authorization group' }, + Supplier: { type: 'string', description: 'Linked supplier account number' }, + FiscalAddress: { type: 'string', description: 'Fiscal address ID' }, + Industry: { type: 'string', description: 'Industry key' }, + NielsenRegion: { type: 'string', description: 'Nielsen ID' }, + ResponsibleType: { type: 'string', description: 'Responsible type' }, + NFPartnerIsNaturalPerson: { type: 'string', description: 'Natural person indicator' }, + InternationalLocationNumber1: { + type: 'string', + description: 'International location number 1', + }, + TaxNumberType: { type: 'string', description: 'Tax number type' }, + VATRegistration: { type: 'string', description: 'VAT registration number' }, + DeletionIndicator: { type: 'boolean', description: 'Central deletion flag' }, + OrderIsBlockedForCustomer: { + type: 'string', + description: 'Central order block reason code', + }, + PostingIsBlocked: { type: 'boolean', description: 'Central posting block flag' }, + DeliveryIsBlocked: { type: 'string', description: 'Central delivery block reason code' }, + BillingIsBlockedForCustomer: { + type: 'string', + description: 'Central billing block reason code', + }, + CreationDate: { type: 'string', description: 'Creation date (OData v2 epoch)' }, + CreatedByUser: { type: 'string', description: 'User who created the customer' }, + }, + }, }, } diff --git a/apps/sim/tools/sap_s4hana/get_inbound_delivery.ts b/apps/sim/tools/sap_s4hana/get_inbound_delivery.ts index 78d78a24596..7fe3ee75f16 100644 --- a/apps/sim/tools/sap_s4hana/get_inbound_delivery.ts +++ b/apps/sim/tools/sap_s4hana/get_inbound_delivery.ts @@ -17,26 +17,26 @@ export const getInboundDeliveryTool: ToolConfig = { params: { subdomain: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)', }, region: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'BTP region (e.g. eu10, us10)', }, clientId: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client ID from the S/4HANA Communication Arrangement', }, clientSecret: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client secret from the S/4HANA Communication Arrangement', }, @@ -110,6 +110,103 @@ export const getProductTool: ToolConfig = { transformResponse: transformSapProxyResponse, outputs: { status: { type: 'number', description: 'HTTP status code returned by SAP' }, - data: { type: 'json', description: 'A_Product entity' }, + data: { + type: 'json', + description: 'OData v2 response envelope; entity at output.data.d', + properties: { + d: { + type: 'json', + description: 'A_Product entity', + properties: { + Product: { + type: 'string', + description: 'Product (material) number', + optional: true, + }, + ProductType: { + type: 'string', + description: 'Product type (e.g., FERT, HAWA)', + optional: true, + }, + ProductGroup: { type: 'string', description: 'Material group', optional: true }, + BaseUnit: { type: 'string', description: 'Base unit of measure', optional: true }, + Brand: { type: 'string', description: 'Brand', optional: true }, + Division: { type: 'string', description: 'Division', optional: true }, + GrossWeight: { type: 'string', description: 'Gross weight', optional: true }, + NetWeight: { type: 'string', description: 'Net weight', optional: true }, + WeightUnit: { + type: 'string', + description: 'Weight unit of measure', + optional: true, + }, + CrossPlantStatus: { + type: 'string', + description: 'Cross-plant material status', + optional: true, + }, + IsMarkedForDeletion: { + type: 'boolean', + description: 'Deletion flag', + optional: true, + }, + ProductStandardID: { + type: 'string', + description: 'Standard product ID (e.g., GTIN)', + optional: true, + }, + ItemCategoryGroup: { + type: 'string', + description: 'Item category group', + optional: true, + }, + ProductOldID: { + type: 'string', + description: 'Legacy/old product ID', + optional: true, + }, + CreatedByUser: { + type: 'string', + description: 'User who created the product', + optional: true, + }, + CreationDate: { + type: 'string', + description: 'Creation date (OData /Date(ms)/)', + optional: true, + }, + LastChangedByUser: { + type: 'string', + description: 'User who last changed the product', + optional: true, + }, + LastChangeDate: { + type: 'string', + description: 'Last change date', + optional: true, + }, + LastChangeDateTime: { + type: 'string', + description: 'Last change timestamp (Edm.DateTimeOffset)', + optional: true, + }, + to_Description: { + type: 'json', + description: 'Product descriptions (when $expand=to_Description)', + optional: true, + }, + to_Plant: { + type: 'json', + description: 'Plant-level data (when $expand=to_Plant)', + optional: true, + }, + to_ProductSales: { + type: 'json', + description: 'Sales data (when $expand=to_ProductSales)', + optional: true, + }, + }, + }, + }, + }, }, } diff --git a/apps/sim/tools/sap_s4hana/get_purchase_order.ts b/apps/sim/tools/sap_s4hana/get_purchase_order.ts index 3a97e272113..e69f45ebeac 100644 --- a/apps/sim/tools/sap_s4hana/get_purchase_order.ts +++ b/apps/sim/tools/sap_s4hana/get_purchase_order.ts @@ -17,26 +17,26 @@ export const getPurchaseOrderTool: ToolConfig ({ ...baseProxyBody(params), service: 'API_PURCHASEORDER_PROCESS_SRV', - path: `/A_PurchaseOrder(${quoteOdataKey(params.purchaseOrder)})`, + path: `/A_PurchaseOrder(${quoteOdataKey(params.purchaseOrder.trim())})`, method: 'GET', query: buildEntityQuery(params), }), @@ -110,6 +110,78 @@ export const getPurchaseOrderTool: ToolConfig = params: { subdomain: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)', }, region: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'BTP region (e.g. eu10, us10)', }, clientId: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client ID from the S/4HANA Communication Arrangement', }, clientSecret: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client secret from the S/4HANA Communication Arrangement', }, @@ -111,6 +111,166 @@ export const getSupplierTool: ToolConfig = transformResponse: transformSapProxyResponse, outputs: { status: { type: 'number', description: 'HTTP status code returned by SAP' }, - data: { type: 'json', description: 'A_Supplier entity' }, + data: { + type: 'json', + description: 'OData v2 response envelope; entity at output.data.d', + properties: { + d: { + type: 'json', + description: 'A_Supplier entity', + properties: { + Supplier: { type: 'string', description: 'Supplier key (up to 10 characters)' }, + AlternativePayeeAccountNumber: { + type: 'string', + description: 'Account number of the alternative payee', + optional: true, + }, + AuthorizationGroup: { + type: 'string', + description: 'Authorization group', + optional: true, + }, + BusinessPartner: { + type: 'string', + description: 'Linked BusinessPartner key', + optional: true, + }, + BR_TaxIsSplit: { + type: 'boolean', + description: 'Brazil-specific tax split flag', + optional: true, + }, + CreatedByUser: { + type: 'string', + description: 'User who created the supplier', + optional: true, + }, + CreationDate: { + type: 'string', + description: 'Creation date (OData v2 epoch)', + optional: true, + }, + Customer: { + type: 'string', + description: 'Linked customer key (if any)', + optional: true, + }, + DeletionIndicator: { + type: 'boolean', + description: 'Central deletion flag', + optional: true, + }, + BirthDate: { + type: 'string', + description: 'Date of birth (OData v2 epoch)', + optional: true, + }, + ConcatenatedInternationalLocNo: { + type: 'string', + description: 'Concatenated international location number', + optional: true, + }, + FiscalAddress: { + type: 'string', + description: 'Fiscal address number', + optional: true, + }, + Industry: { type: 'string', description: 'Industry key', optional: true }, + InternationalLocationNumber1: { + type: 'string', + description: 'International location number, part 1', + optional: true, + }, + InternationalLocationNumber2: { + type: 'string', + description: 'International location number, part 2', + optional: true, + }, + InternationalLocationNumber3: { + type: 'string', + description: 'International location number, part 3', + optional: true, + }, + IsNaturalPerson: { + type: 'boolean', + description: 'Indicates whether the supplier is a natural person', + optional: true, + }, + PaymentIsBlockedForSupplier: { + type: 'boolean', + description: 'Payment block flag', + optional: true, + }, + PostingIsBlocked: { + type: 'boolean', + description: 'Posting block flag', + optional: true, + }, + PurchasingIsBlocked: { + type: 'boolean', + description: 'Purchasing block flag', + optional: true, + }, + ResponsibleType: { + type: 'string', + description: 'Type of business (Brazil)', + optional: true, + }, + SupplierAccountGroup: { + type: 'string', + description: 'Supplier account group', + optional: true, + }, + SupplierCorporateGroup: { + type: 'string', + description: 'Corporate group identifier', + optional: true, + }, + SupplierFullName: { + type: 'string', + description: 'Full name of the supplier', + optional: true, + }, + SupplierName: { type: 'string', description: 'Supplier name', optional: true }, + SupplierProcurementBlock: { + type: 'string', + description: 'Procurement block at supplier level', + optional: true, + }, + SuplrProofOfDelivRlvtCode: { + type: 'string', + description: 'Proof of delivery relevance code', + optional: true, + }, + SuplrQltyInProcmtCertfnValidTo: { + type: 'string', + description: 'Quality certification validity end date (OData v2 epoch)', + optional: true, + }, + SuplrQualityManagementSystem: { + type: 'string', + description: 'Quality management system of the supplier', + optional: true, + }, + TaxNumber1: { type: 'string', description: 'Tax number 1', optional: true }, + TaxNumber2: { type: 'string', description: 'Tax number 2', optional: true }, + TaxNumber3: { type: 'string', description: 'Tax number 3', optional: true }, + TaxNumber4: { type: 'string', description: 'Tax number 4', optional: true }, + TaxNumber5: { type: 'string', description: 'Tax number 5', optional: true }, + TaxNumberResponsible: { + type: 'string', + description: 'Tax number of responsible party', + optional: true, + }, + TaxNumberType: { type: 'string', description: 'Tax number type', optional: true }, + VATRegistration: { + type: 'string', + description: 'VAT registration number', + optional: true, + }, + }, + }, + }, + }, }, } diff --git a/apps/sim/tools/sap_s4hana/get_supplier_invoice.ts b/apps/sim/tools/sap_s4hana/get_supplier_invoice.ts index b27bb533e5b..2c66d1adeda 100644 --- a/apps/sim/tools/sap_s4hana/get_supplier_invoice.ts +++ b/apps/sim/tools/sap_s4hana/get_supplier_invoice.ts @@ -17,26 +17,26 @@ export const getSupplierInvoiceTool: ToolConfig ({ ...baseProxyBody(params), service: 'API_SUPPLIERINVOICE_PROCESS_SRV', - path: `/A_SupplierInvoice(FiscalYear=${quoteOdataKey(params.fiscalYear)},SupplierInvoice=${quoteOdataKey(params.supplierInvoice)})`, + path: `/A_SupplierInvoice(SupplierInvoice=${quoteOdataKey(params.supplierInvoice)},FiscalYear=${quoteOdataKey(params.fiscalYear)})`, method: 'GET', query: buildEntityQuery(params), }), @@ -116,6 +116,81 @@ export const getSupplierInvoiceTool: ToolConfig params: { subdomain: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)', }, region: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'BTP region (e.g. eu10, us10)', }, clientId: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client ID from the S/4HANA Communication Arrangement', }, clientSecret: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client secret from the S/4HANA Communication Arrangement', }, @@ -127,6 +127,123 @@ export const listProductsTool: ToolConfig transformResponse: transformSapProxyResponse, outputs: { status: { type: 'number', description: 'HTTP status code returned by SAP' }, - data: { type: 'json', description: 'Array of A_Product entities' }, + data: { + type: 'json', + description: 'OData v2 response envelope; collection at output.data.d.results', + properties: { + d: { + type: 'json', + description: 'OData v2 envelope', + properties: { + results: { + type: 'array', + description: 'A_Product entities', + items: { + type: 'object', + properties: { + Product: { + type: 'string', + description: 'Product (material) number', + optional: true, + }, + ProductType: { + type: 'string', + description: 'Product type (e.g., FERT, HAWA)', + optional: true, + }, + ProductGroup: { + type: 'string', + description: 'Material group', + optional: true, + }, + BaseUnit: { + type: 'string', + description: 'Base unit of measure', + optional: true, + }, + Brand: { type: 'string', description: 'Brand', optional: true }, + Division: { type: 'string', description: 'Division', optional: true }, + GrossWeight: { + type: 'string', + description: 'Gross weight', + optional: true, + }, + NetWeight: { + type: 'string', + description: 'Net weight', + optional: true, + }, + WeightUnit: { + type: 'string', + description: 'Weight unit of measure', + optional: true, + }, + CrossPlantStatus: { + type: 'string', + description: 'Cross-plant material status', + optional: true, + }, + IsMarkedForDeletion: { + type: 'boolean', + description: 'Deletion flag', + optional: true, + }, + ProductStandardID: { + type: 'string', + description: 'Standard product ID (e.g., GTIN)', + optional: true, + }, + ItemCategoryGroup: { + type: 'string', + description: 'Item category group', + optional: true, + }, + ProductOldID: { + type: 'string', + description: 'Legacy/old product ID', + optional: true, + }, + CreatedByUser: { + type: 'string', + description: 'User who created the product', + optional: true, + }, + CreationDate: { + type: 'string', + description: 'Creation date (OData /Date(ms)/)', + optional: true, + }, + LastChangedByUser: { + type: 'string', + description: 'User who last changed the product', + optional: true, + }, + LastChangeDate: { + type: 'string', + description: 'Last change date', + optional: true, + }, + LastChangeDateTime: { + type: 'string', + description: 'Last change timestamp (Edm.DateTimeOffset)', + optional: true, + }, + }, + }, + }, + __next: { + type: 'string', + description: 'OData skiptoken URL for next page', + optional: true, + }, + __count: { + type: 'string', + description: 'Total count when $inlinecount=allpages is used', + optional: true, + }, + }, + }, + }, + }, }, } diff --git a/apps/sim/tools/sap_s4hana/list_purchase_orders.ts b/apps/sim/tools/sap_s4hana/list_purchase_orders.ts index f3e2c7778d9..fc2e829ab43 100644 --- a/apps/sim/tools/sap_s4hana/list_purchase_orders.ts +++ b/apps/sim/tools/sap_s4hana/list_purchase_orders.ts @@ -16,26 +16,26 @@ export const listPurchaseOrdersTool: ToolConfig = { id: 'sap_s4hana_odata_query', name: 'SAP S/4HANA OData Query', description: - 'Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping.', + 'Make an arbitrary OData v2 call against any SAP S/4HANA Cloud whitelisted Communication Scenario. Use when no dedicated tool exists for the entity. The proxy handles auth, CSRF, and OData unwrapping. For write operations (POST/PUT/PATCH/MERGE/DELETE), pass an If-Match ETag obtained from a prior GET to avoid lost updates; misuse will mutate production data.', version: '1.0.0', params: { subdomain: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'SAP BTP subaccount subdomain (technical name of your subaccount, not the S/4HANA host)', }, region: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'BTP region (e.g. eu10, us10)', }, clientId: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client ID from the S/4HANA Communication Arrangement', }, clientSecret: { type: 'string', - required: true, + required: false, visibility: 'user-only', description: 'OAuth client secret from the S/4HANA Communication Arrangement', }, diff --git a/apps/sim/tools/sap_s4hana/update_business_partner.ts b/apps/sim/tools/sap_s4hana/update_business_partner.ts index 41dd9b5165d..cb37c577f0d 100644 --- a/apps/sim/tools/sap_s4hana/update_business_partner.ts +++ b/apps/sim/tools/sap_s4hana/update_business_partner.ts @@ -13,31 +13,31 @@ export const updateBusinessPartnerTool: ToolConfig Date: Wed, 6 May 2026 20:24:40 -0700 Subject: [PATCH 15/17] feat(files): zoom controls for inline mermaid and images in markdown (#4411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add pan/zoom/fit controls to mermaid diagrams rendered inline in markdown — same experience as the standalone .mmd viewer - Wrap inline markdown images in ZoomablePreview with fit-to-container scale - Allow fit zoom to upscale small diagrams to fill the view (previously capped at 100%) --- .../components/file-viewer/preview-panel.tsx | 24 +++++++++++++------ .../file-viewer/zoomable-preview.tsx | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx index 36966864095..75e49d38494 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx @@ -495,7 +495,14 @@ const STATIC_MARKDOWN_COMPONENTS = { ), 'mermaid-diagram': ({ definition }: { definition?: string }) => { const isStreaming = useContext(MermaidStreamingCtx) - return + return ( + + ) }, p: ({ children }: { children?: React.ReactNode }) => (

@@ -619,12 +626,15 @@ const STATIC_MARKDOWN_COMPONENTS = { img: ({ src, alt }: React.ImgHTMLAttributes) => { const resolvedSrc = resolveSimFileUrl(typeof src === 'string' ? src : undefined) return ( - {alt + + {alt + ) }, table: ({ children }: { children?: React.ReactNode }) => ( diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/zoomable-preview.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/zoomable-preview.tsx index bcdb3aa9e06..151b4325a8d 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/zoomable-preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/zoomable-preview.tsx @@ -47,7 +47,7 @@ function getFitZoom(container: Size, content: Size): number { const availableWidth = Math.max(1, container.width - FIT_PADDING) const availableHeight = Math.max(1, container.height - FIT_PADDING) - return clampZoom(Math.min(1, availableWidth / content.width, availableHeight / content.height)) + return clampZoom(Math.min(availableWidth / content.width, availableHeight / content.height)) } function clampOffset(container: Size, content: Size, offset: Offset, zoom: number): Offset { From 28e60bfd4ea46affab325ea1f5d2d7ac44fc2e3c Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 6 May 2026 20:25:07 -0700 Subject: [PATCH 16/17] fix(docker): drop scripts/ from workspaces array (#4484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `turbo prune sim --docker` strips `scripts/` from the pruned output (sim doesn't depend on it), but the pruned root package.json still listed it as a workspace, causing `bun install` to fail with "Workspace not found 'scripts'" in the Docker build. scripts/ is dev-only tooling that runs from the repo root via `bun run scripts/*.ts`. Its imports (glob, yaml) resolve against the root node_modules — they're already in root devDependencies. - Remove "scripts" from root workspaces array - Delete scripts/package.json (no longer a workspace, manifest unused) Co-authored-by: Claude Opus 4.7 --- bun.lock | 12 ------------ package.json | 3 +-- scripts/package.json | 11 ----------- 3 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 scripts/package.json diff --git a/bun.lock b/bun.lock index 0e2c363e7f0..3a1ac7226c5 100644 --- a/bun.lock +++ b/bun.lock @@ -477,14 +477,6 @@ "reactflow": "^11.11.4", }, }, - "scripts": { - "name": "sim-doc-generator", - "version": "1.0.0", - "dependencies": { - "glob": "^11.1.0", - "yaml": "^2.8.1", - }, - }, }, "trustedDependencies": [ "ffmpeg-static", @@ -3697,8 +3689,6 @@ "sim": ["sim@workspace:apps/sim"], - "sim-doc-generator": ["sim-doc-generator@workspace:scripts"], - "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], @@ -4889,8 +4879,6 @@ "sim/tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], - "sim-doc-generator/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], - "simstudio/@types/node": ["@types/node@20.19.39", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw=="], "simstudio/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], diff --git a/package.json b/package.json index 21d2cc7b80a..12061f7cdc3 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,7 @@ "license": "Apache-2.0", "workspaces": [ "apps/*", - "packages/*", - "scripts" + "packages/*" ], "scripts": { "build": "turbo run build", diff --git a/scripts/package.json b/scripts/package.json deleted file mode 100644 index e2d16dfbdb2..00000000000 --- a/scripts/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "sim-doc-generator", - "version": "1.0.0", - "description": "Documentation generator and contract sync scripts for Sim", - "type": "module", - "private": true, - "dependencies": { - "glob": "^11.1.0", - "yaml": "^2.8.1" - } -} From 76d602fed5841e1ffae9d74a1af5d7691194d7e1 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 6 May 2026 21:03:15 -0700 Subject: [PATCH 17/17] fix(workday): correct SOAP service routing and reference types (#4485) * fix(workday): correct SOAP service routing and reference types - create-prehire: route Put_Applicant to Recruiting service (was Staffing, where the operation does not exist) - assign-onboarding: use WID for Action_Event_Reference (was Background_Check_ID, wrong identifier domain for hire events) - update-worker block: rewrite labels and wand prompt to match Change_Personal_Information demographic-only scope (prior prompt instructed LLM to emit businessTitle/primaryWorkEmail which the SOAP op rejects) - enrich opaque JSON output descriptions on worker, workers, organizations, compensationPlans * fix(workday): correct Date_of_Birth casing in update wand prompt --- .../tools/workday/assign-onboarding/route.ts | 2 +- .../api/tools/workday/create-prehire/route.ts | 2 +- apps/sim/blocks/blocks/workday.ts | 51 +++++++++++++------ 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/apps/sim/app/api/tools/workday/assign-onboarding/route.ts b/apps/sim/app/api/tools/workday/assign-onboarding/route.ts index 618a3bd5d36..044dc7c662c 100644 --- a/apps/sim/app/api/tools/workday/assign-onboarding/route.ts +++ b/apps/sim/app/api/tools/workday/assign-onboarding/route.ts @@ -36,7 +36,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { Onboarding_Plan_Assignment_Data: { Onboarding_Plan_Reference: wdRef('Onboarding_Plan_ID', data.onboardingPlanId), Person_Reference: wdRef('WID', data.workerId), - Action_Event_Reference: wdRef('Background_Check_ID', data.actionEventId), + Action_Event_Reference: wdRef('WID', data.actionEventId), Assignment_Effective_Moment: new Date().toISOString(), Active: true, }, diff --git a/apps/sim/app/api/tools/workday/create-prehire/route.ts b/apps/sim/app/api/tools/workday/create-prehire/route.ts index c7937807040..fae29cdd054 100644 --- a/apps/sim/app/api/tools/workday/create-prehire/route.ts +++ b/apps/sim/app/api/tools/workday/create-prehire/route.ts @@ -48,7 +48,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const client = await createWorkdaySoapClient( data.tenantUrl, data.tenant, - 'staffing', + 'recruiting', data.username, data.password ) diff --git a/apps/sim/blocks/blocks/workday.ts b/apps/sim/blocks/blocks/workday.ts index 76d54900afb..8b27c8a94b9 100644 --- a/apps/sim/blocks/blocks/workday.ts +++ b/apps/sim/blocks/blocks/workday.ts @@ -24,7 +24,7 @@ export const WorkdayBlock: BlockConfig = { { label: 'List Workers', id: 'list_workers' }, { label: 'Create Pre-Hire', id: 'create_prehire' }, { label: 'Hire Employee', id: 'hire_employee' }, - { label: 'Update Worker', id: 'update_worker' }, + { label: 'Update Personal Information', id: 'update_worker' }, { label: 'Assign Onboarding Plan', id: 'assign_onboarding' }, { label: 'Get Organizations', id: 'get_organizations' }, { label: 'Change Job', id: 'change_job' }, @@ -228,31 +228,38 @@ export const WorkdayBlock: BlockConfig = { // Update Worker { id: 'fields', - title: 'Fields (JSON)', + title: 'Personal Information (JSON)', type: 'code', language: 'json', placeholder: - '{\n "businessTitle": "Senior Engineer",\n "primaryWorkEmail": "new@company.com"\n}', + '{\n "Marital_Status_Reference": {\n "ID": { "attributes": { "wd:type": "Marital_Status_ID" }, "$value": "Married" }\n }\n}', + description: + 'Demographic fields supported by Workday Change_Personal_Information (e.g. Date_of_Birth, Gender_Reference, Marital_Status_Reference, Ethnicity_Reference, Citizenship_Status_Reference). Does not update business title or work contact info.', condition: { field: 'operation', value: 'update_worker' }, required: { field: 'operation', value: 'update_worker' }, wandConfig: { enabled: true, maintainHistory: true, - prompt: `Generate a Workday worker update payload as JSON. + prompt: `Generate a Workday Personal_Information_Data payload as JSON for the Change_Personal_Information SOAP operation. -### COMMON FIELDS -- businessTitle: Job title string -- primaryWorkEmail: Work email address -- primaryWorkPhone: Work phone number -- managerReference: Manager worker ID +### SUPPORTED FIELDS (demographics only) +- Date_of_Birth: ISO date string (YYYY-MM-DD) +- Gender_Reference: { ID: { attributes: { "wd:type": "Gender_Code" }, $value: "Male" | "Female" | ... } } +- Marital_Status_Reference: { ID: { attributes: { "wd:type": "Marital_Status_ID" }, $value: "Married" | "Single" | ... } } +- Ethnicity_Reference: { ID: { attributes: { "wd:type": "Ethnicity_ID" }, $value: "..." } } +- Citizenship_Status_Reference: same shape + +### NOT SUPPORTED BY THIS OPERATION +- Business title (use Change_Job) +- Work email / phone / manager (different SOAP ops) ### RULES - Output ONLY valid JSON starting with { and ending with } - Include only fields that need updating ### EXAMPLE -User: "Update title to Senior Engineer" -Output: {"businessTitle": "Senior Engineer"}`, +User: "Mark marital status as Married" +Output: {"Marital_Status_Reference":{"ID":{"attributes":{"wd:type":"Marital_Status_ID"},"$value":"Married"}}}`, generationType: 'json-object', }, }, @@ -424,8 +431,16 @@ Output: {"businessTitle": "Senior Engineer"}`, lastDayOfWork: { type: 'string', description: 'Last day of work' }, }, outputs: { - worker: { type: 'json', description: 'Worker profile data' }, - workers: { type: 'json', description: 'Array of worker profiles' }, + worker: { + type: 'json', + description: + 'Worker profile (id, descriptor, primaryWorkEmail, primaryWorkPhone, businessTitle, supervisoryOrganization, hireDate, workerType, isActive)', + }, + workers: { + type: 'json', + description: + 'Array of worker profiles (id, descriptor, primaryWorkEmail, businessTitle, supervisoryOrganization, hireDate, workerType, isActive)', + }, total: { type: 'number', description: 'Total count of results' }, preHireId: { type: 'string', description: 'Created pre-hire ID' }, descriptor: { type: 'string', description: 'Display name of pre-hire' }, @@ -434,10 +449,16 @@ Output: {"businessTitle": "Senior Engineer"}`, hireDate: { type: 'string', description: 'Hire date' }, assignmentId: { type: 'string', description: 'Onboarding assignment ID' }, planId: { type: 'string', description: 'Onboarding plan ID' }, - organizations: { type: 'json', description: 'Array of organizations' }, + organizations: { + type: 'json', + description: 'Array of organizations (id, descriptor, type, subtype, isActive)', + }, eventId: { type: 'string', description: 'Event ID for staffing changes' }, effectiveDate: { type: 'string', description: 'Effective date of change' }, - compensationPlans: { type: 'json', description: 'Compensation plan details' }, + compensationPlans: { + type: 'json', + description: 'Compensation plans (id, planName, amount, currency, frequency)', + }, terminationDate: { type: 'string', description: 'Termination date' }, }, }