Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/docs/components/ui/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
onedrive: MicrosoftOneDriveIcon,
onepassword: OnePasswordIcon,
openai: OpenAIIcon,
oracle_fusion_financials: NetSuiteIcon,
outlook: OutlookIcon,
pagerduty: PagerDutyIcon,
parallel_ai: ParallelIcon,
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/integrations/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"onedrive",
"onepassword",
"openai",
"oracle_fusion_financials",
"outlook",
"pagerduty",
"parallel_ai",
Expand Down
2,447 changes: 2,447 additions & 0 deletions apps/docs/content/docs/integrations/oracle_fusion_financials.mdx

Large diffs are not rendered by default.

6,049 changes: 6,049 additions & 0 deletions apps/sim/blocks/blocks/oracle_fusion_financials.ts

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions apps/sim/blocks/registry-maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,10 @@ import { OktaBlock, OktaBlockMeta } from '@/blocks/blocks/okta'
import { OneDriveBlock, OneDriveBlockMeta } from '@/blocks/blocks/onedrive'
import { OnePasswordBlock, OnePasswordBlockMeta } from '@/blocks/blocks/onepassword'
import { OpenAIBlock, OpenAIBlockMeta } from '@/blocks/blocks/openai'
import {
OracleFusionFinancialsBlock,
OracleFusionFinancialsBlockMeta,
} from '@/blocks/blocks/oracle_fusion_financials'
import { OutlookBlock, OutlookBlockMeta } from '@/blocks/blocks/outlook'
import { PagerDutyBlock, PagerDutyBlockMeta } from '@/blocks/blocks/pagerduty'
import { ParallelBlock, ParallelBlockMeta } from '@/blocks/blocks/parallel'
Expand Down Expand Up @@ -593,6 +597,7 @@ export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
onedrive: OneDriveBlock,
onepassword: OnePasswordBlock,
openai: OpenAIBlock,
oracle_fusion_financials: OracleFusionFinancialsBlock,
outlook: OutlookBlock,
pagerduty: PagerDutyBlock,
parallel_ai: ParallelBlock,
Expand Down Expand Up @@ -914,6 +919,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
onedrive: OneDriveBlockMeta,
onepassword: OnePasswordBlockMeta,
openai: OpenAIBlockMeta,
oracle_fusion_financials: OracleFusionFinancialsBlockMeta,
outlook: OutlookBlockMeta,
pagerduty: PagerDutyBlockMeta,
parallel_ai: ParallelBlockMeta,
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/copilot/generated/docs-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export const DOCS_MANIFEST: readonly string[] = [
'integrations/onedrive.mdx',
'integrations/onepassword.mdx',
'integrations/openai.mdx',
'integrations/oracle_fusion_financials.mdx',
'integrations/outlook.mdx',
'integrations/pagerduty.mdx',
'integrations/parallel_ai.mdx',
Expand Down
15 changes: 15 additions & 0 deletions apps/sim/lib/integrations/credential-display.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const EXPECTED_COVERAGE: Record<string, string[]> = {
// NetSuite remains an API-key catalog integration, like Snowflake, while its
// block uses the shared reusable-credential selector.
'netsuite-service-account': [],
'oracle-fusion-service-account': [],
'pipedrive-service-account': ['pipedrive'],
'salesforce-service-account': ['salesforce'],
'shopify-service-account': ['shopify'],
Expand Down Expand Up @@ -110,6 +111,20 @@ describe('service-account coverage', () => {
})
})

it('exposes Oracle Fusion reusable credentials without changing its API-key catalog class', () => {
const integration = INTEGRATIONS.find(
(candidate) => candidate.type === 'oracle_fusion_financials'
)
expect(integration?.authType).toBe('api-key')
expect(
OAUTH_PROVIDERS.oracle_fusion_financials.services.oracle_fusion_financials
).toMatchObject({
providerId: 'oracle_fusion_financials',
serviceAccountProviderId: 'oracle-fusion-service-account',
authType: 'service_account',
})
})

it('pins the table to exactly the registered service-account provider ids', () => {
expect(REGISTERED_SERVICE_ACCOUNT_IDS).toEqual(Object.keys(EXPECTED_COVERAGE).sort())
})
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/integrations/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
okta: OktaIcon,
onedrive: MicrosoftOneDriveIcon,
onepassword: OnePasswordIcon,
oracle_fusion_financials: NetSuiteIcon,
outlook: OutlookIcon,
pagerduty: PagerDutyIcon,
parallel_ai: ParallelIcon,
Expand Down
278 changes: 278 additions & 0 deletions apps/sim/lib/internal/oracle-fusion-financials/execute-tool.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockRequest } = vi.hoisted(() => ({ mockRequest: vi.fn() }))
vi.mock('@/lib/internal/oracle-fusion/client', () => ({
requestOracleFusionJson: mockRequest,
}))

import type { OracleFusionRequest } from '@/lib/internal/oracle-fusion/client'
import { OracleFusionProviderError } from '@/lib/internal/oracle-fusion/errors'
import { executeOracleFusionFinancialsTool } from '@/lib/internal/oracle-fusion-financials/execute-tool'
import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types'
import { OracleFusionFinancialsBlock } from '@/blocks/blocks/oracle_fusion_financials'
import * as financialsTools from '@/tools/oracle_fusion_financials'

const ORIGIN = 'https://vision.fa.us2.oraclecloud.com'
const AUTH = {
oauthCredential: 'credential-1',
accessToken: 'secret-credential-canary',
instanceUrl: ORIGIN,
}

function call(overrides: Partial<InternalToolOperationCall> = {}): InternalToolOperationCall {
return {
toolId: 'oracle_fusion_financials_list_payables_invoices',
input: AUTH,
headers: new Headers(),
context: { workflowId: 'workflow-1', userId: 'user-1', workspaceId: 'workspace-1' },
requestId: 'request-1',
...overrides,
}
}

describe('Oracle Fusion Financials execution boundary', () => {
beforeEach(() => {
mockRequest.mockReset()
mockRequest.mockImplementation((_credential: unknown, request: OracleFusionRequest) => {
if (request.query?.limit !== undefined) {
return { items: [], count: 0, hasMore: false, limit: 50, offset: 0 }
}
return {
'@context': {
links: [
{
rel: 'self',
href: `${ORIGIN}/fscmRestApi/resources/11.13.18.05/${request.address.relativePath}`,
},
],
},
accessToken: AUTH.accessToken,
}
})
})

it.each(
Object.values(financialsTools).filter((tool) =>
/_payables_|_payment_process_request/.test(tool.id)
)
)('executes the real $id declaration and operation', async (tool) => {
const params = {
...AUTH,
invoiceUniqId: 'invoice-key',
invoiceLineUniqId: 'line-key',
invoiceInstallmentUniqId: 'installment-key',
invoiceDistributionId: '99',
appliedPrepaymentUniqId: 'applied-key',
availablePrepaymentUniqId: 'available-key',
checkId: '42',
invoicePaymentId: '88',
holdId: '21',
paymentProcessRequestId: '17',
termsId: '73',
paymentTermLineUniqId: 'term-line-key',
}
const input = tool.operation.input(params)
const response = await executeOracleFusionFinancialsTool(call({ toolId: tool.id, input }))
expect(response.status).toBe(200)
const result = await response.json()
expect(result.success).toBe(true)
expect(mockRequest).toHaveBeenCalledTimes(1)
expect(JSON.stringify(result)).not.toMatch(/secret-credential-canary|accessToken|instanceUrl/)
})

it('distinguishes invalid inputs from malformed provider payloads without reflecting either', async () => {
const invalid = await executeOracleFusionFinancialsTool(
call({ input: { ...AUTH, limit: 101 } })
)
expect(invalid.status).toBe(400)
await expect(invalid.json()).resolves.toEqual({
success: false,
output: {},
error: 'Invalid Oracle Fusion Financials input',
})
expect(mockRequest).not.toHaveBeenCalled()

mockRequest.mockResolvedValue({ items: [AUTH], count: 'secret-provider-value' })
const malformed = await executeOracleFusionFinancialsTool(call())
expect(malformed.status).toBe(502)
await expect(malformed.json()).resolves.toEqual({
success: false,
output: {},
error: 'Oracle Fusion Financials returned an unexpected response shape',
})
})

it('preserves the safe shared provider error and hides unexpected internal failures', async () => {
mockRequest.mockRejectedValueOnce(
new OracleFusionProviderError('Oracle Fusion request failed', 403)
)
const provider = await executeOracleFusionFinancialsTool(call())
expect(provider.status).toBe(403)
await expect(provider.json()).resolves.toMatchObject({ error: 'Oracle Fusion request failed' })

mockRequest.mockRejectedValueOnce(new Error(AUTH.accessToken))
const internal = await executeOracleFusionFinancialsTool(call())
expect(internal.status).toBe(500)
await expect(internal.json()).resolves.toEqual({
success: false,
output: {},
error: 'Oracle Fusion Financials request failed',
})
})

it('forwards cancellation and never reports an aborted request as an ordinary failure', async () => {
const controller = new AbortController()
const reason = new Error('cancelled')
mockRequest.mockImplementationOnce((_auth, _request, signal: AbortSignal) => {
expect(signal).toBe(controller.signal)
controller.abort(reason)
throw reason
})
await expect(
executeOracleFusionFinancialsTool(call({ signal: controller.signal }))
).rejects.toBe(reason)
mockRequest.mockClear()
await expect(
executeOracleFusionFinancialsTool(call({ signal: controller.signal }))
).rejects.toBe(reason)
expect(mockRequest).not.toHaveBeenCalled()
})

it('rejects unsupported operations without sending a provider request', async () => {
const result = await executeOracleFusionFinancialsTool(
call({ toolId: 'oracle_fusion_financials_delete_invoice' })
)
expect(result.status).toBe(500)
expect(mockRequest).not.toHaveBeenCalled()
})

it('coerces block controls only at execution and preserves opaque manual keys', () => {
const config = OracleFusionFinancialsBlock.tools.config!
const params = {
operation: 'oracle_fusion_financials_list_payables_invoice_lines',
invoiceUniqId: ' opaque%2Fkey ',
limit: '25',
offset: '50',
totalResults: 'true',
}
expect(config.tool(params)).toBe(params.operation)
expect(config.params!(params)).toMatchObject({
invoiceUniqId: ' opaque%2Fkey ',
limit: 25,
offset: 50,
totalResults: true,
})
expect(config.tool({ ...params, limit: 'invalid' })).toBe(params.operation)
expect(() => config.params!({ ...params, limit: 'invalid' })).toThrow()
})

it('coerces write inputs only at execution without rounding identifiers or losing explicit nulls', () => {
const config = OracleFusionFinancialsBlock.tools.config!
const params = {
operation: 'oracle_fusion_financials_apply_receivables_receipt',
receivablesReceiptId: '42',
appliedPaymentScheduleId: '9007199254740993',
amountApplied: '12.5',
}
expect(config.tool({ ...params, amountApplied: 'invalid' })).toBe(params.operation)
expect(config.params!(params)).toMatchObject({
appliedPaymentScheduleId: '9007199254740993',
amountApplied: 12.5,
})
expect(() => config.params!({ ...params, amountApplied: 'invalid' })).toThrow()
expect(
config.params!({
operation: 'oracle_fusion_financials_update_receivables_receipt',
receivablesReceiptId: '42',
conversionRate: null,
})
).toMatchObject({ conversionRate: null })
expect(
config.params!({
operation: 'oracle_fusion_financials_create_receivables_invoice',
lines: '[{"LineNumber":1,"Quantity":2}]',
})
).toMatchObject({ lines: [{ LineNumber: 1, Quantity: 2 }] })
})

it('dispatches receipt application and returns a typed business failure without credential data', async () => {
mockRequest.mockResolvedValueOnce({ result: 'ERROR', accessToken: AUTH.accessToken })
const tool = financialsTools.oracleFusionFinancialsApplyReceivablesReceiptTool
const input = tool.operation.input({
...AUTH,
receivablesReceiptId: '42',
appliedPaymentScheduleId: '9007199254740993',
})
const response = await executeOracleFusionFinancialsTool(call({ toolId: tool.id, input }))
expect(response.status).toBe(200)
await expect(response.json()).resolves.toEqual({
success: false,
output: { result: 'ERROR' },
error: 'Oracle Fusion action reported an unsuccessful result',
})
})

it('dispatches report submission and preserves submission errors as a business failure', async () => {
const controller = new AbortController()
mockRequest.mockResolvedValueOnce({ result: '1:EXP-42' })
const tool = financialsTools.oracleFusionFinancialsSubmitExpenseReportTool
const input = tool.operation.input({ ...AUTH, expenseReportUniqId: ' report key ' })
const response = await executeOracleFusionFinancialsTool(
call({ toolId: tool.id, input, signal: controller.signal })
)
expect(response.status).toBe(200)
await expect(response.json()).resolves.toMatchObject({
success: false,
output: { result: '1:EXP-42' },
})
expect(mockRequest).toHaveBeenCalledWith(
expect.objectContaining(AUTH),
expect.objectContaining({
address: {
family: 'fscm',
relativePath: 'expenseReports/%20report%20key%20/action/submit',
},
method: 'POST',
}),
controller.signal
)
})

it('dispatches ledger balances with execution-time pagination and unchanged finder text', async () => {
const tool = financialsTools.oracleFusionFinancialsListGlBalancesTool
const config = OracleFusionFinancialsBlock.tools.config!
const finder = 'AccountGroupBalanceFinder;accountGroupName=Cash,ledgerName=US Primary'
const params = config.params!({
...AUTH,
operation: tool.id,
finder,
limit: '25',
offset: '50',
})
mockRequest.mockResolvedValueOnce({
items: [{ EndingBalance: '#MISSING' }],
count: 1,
hasMore: false,
limit: 25,
offset: 50,
})
const response = await executeOracleFusionFinancialsTool(
call({ toolId: tool.id, input: tool.operation.input(params) })
)
await expect(response.json()).resolves.toMatchObject({
success: true,
output: { items: [{ EndingBalance: '#MISSING' }], limit: 25, offset: 50 },
})
expect(mockRequest).toHaveBeenCalledWith(
expect.objectContaining(AUTH),
expect.objectContaining({
address: { family: 'fscm', relativePath: 'ledgerBalances' },
query: expect.objectContaining({ finder, limit: 25, offset: 50 }),
}),
undefined
)
})
})
Loading
Loading