Skip to content

Commit efd8f7a

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
feat(oracle-fusion-financials): add Payables and Receivables integration
1 parent 15294a2 commit efd8f7a

100 files changed

Lines changed: 15492 additions & 8 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/components/ui/icon-mapping.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
480480
onedrive: MicrosoftOneDriveIcon,
481481
onepassword: OnePasswordIcon,
482482
openai: OpenAIIcon,
483+
oracle_fusion_financials: NetSuiteIcon,
483484
outlook: OutlookIcon,
484485
pagerduty: PagerDutyIcon,
485486
parallel_ai: ParallelIcon,

apps/docs/content/docs/integrations/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@
187187
"onedrive",
188188
"onepassword",
189189
"openai",
190+
"oracle_fusion_financials",
190191
"outlook",
191192
"pagerduty",
192193
"parallel_ai",

apps/docs/content/docs/integrations/oracle_fusion_financials.mdx

Lines changed: 1563 additions & 0 deletions
Large diffs are not rendered by default.

apps/sim/blocks/blocks/oracle_fusion_financials.ts

Lines changed: 3135 additions & 0 deletions
Large diffs are not rendered by default.

apps/sim/blocks/registry-maps.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ import { OktaBlock, OktaBlockMeta } from '@/blocks/blocks/okta'
251251
import { OneDriveBlock, OneDriveBlockMeta } from '@/blocks/blocks/onedrive'
252252
import { OnePasswordBlock, OnePasswordBlockMeta } from '@/blocks/blocks/onepassword'
253253
import { OpenAIBlock, OpenAIBlockMeta } from '@/blocks/blocks/openai'
254+
import {
255+
OracleFusionFinancialsBlock,
256+
OracleFusionFinancialsBlockMeta,
257+
} from '@/blocks/blocks/oracle_fusion_financials'
254258
import { OutlookBlock, OutlookBlockMeta } from '@/blocks/blocks/outlook'
255259
import { PagerDutyBlock, PagerDutyBlockMeta } from '@/blocks/blocks/pagerduty'
256260
import { ParallelBlock, ParallelBlockMeta } from '@/blocks/blocks/parallel'
@@ -593,6 +597,7 @@ export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
593597
onedrive: OneDriveBlock,
594598
onepassword: OnePasswordBlock,
595599
openai: OpenAIBlock,
600+
oracle_fusion_financials: OracleFusionFinancialsBlock,
596601
outlook: OutlookBlock,
597602
pagerduty: PagerDutyBlock,
598603
parallel_ai: ParallelBlock,
@@ -914,6 +919,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
914919
onedrive: OneDriveBlockMeta,
915920
onepassword: OnePasswordBlockMeta,
916921
openai: OpenAIBlockMeta,
922+
oracle_fusion_financials: OracleFusionFinancialsBlockMeta,
917923
outlook: OutlookBlockMeta,
918924
pagerduty: PagerDutyBlockMeta,
919925
parallel_ai: ParallelBlockMeta,

apps/sim/lib/copilot/generated/docs-manifest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ export const DOCS_MANIFEST: readonly string[] = [
243243
'integrations/onedrive.mdx',
244244
'integrations/onepassword.mdx',
245245
'integrations/openai.mdx',
246+
'integrations/oracle_fusion_financials.mdx',
246247
'integrations/outlook.mdx',
247248
'integrations/pagerduty.mdx',
248249
'integrations/parallel_ai.mdx',

apps/sim/lib/integrations/credential-display.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const EXPECTED_COVERAGE: Record<string, string[]> = {
6868
// NetSuite remains an API-key catalog integration, like Snowflake, while its
6969
// block uses the shared reusable-credential selector.
7070
'netsuite-service-account': [],
71+
'oracle-fusion-service-account': [],
7172
'pipedrive-service-account': ['pipedrive'],
7273
'salesforce-service-account': ['salesforce'],
7374
'shopify-service-account': ['shopify'],
@@ -110,6 +111,20 @@ describe('service-account coverage', () => {
110111
})
111112
})
112113

114+
it('exposes Oracle Fusion reusable credentials without changing its API-key catalog class', () => {
115+
const integration = INTEGRATIONS.find(
116+
(candidate) => candidate.type === 'oracle_fusion_financials'
117+
)
118+
expect(integration?.authType).toBe('api-key')
119+
expect(
120+
OAUTH_PROVIDERS.oracle_fusion_financials.services.oracle_fusion_financials
121+
).toMatchObject({
122+
providerId: 'oracle_fusion_financials',
123+
serviceAccountProviderId: 'oracle-fusion-service-account',
124+
authType: 'service_account',
125+
})
126+
})
127+
113128
it('pins the table to exactly the registered service-account provider ids', () => {
114129
expect(REGISTERED_SERVICE_ACCOUNT_IDS).toEqual(Object.keys(EXPECTED_COVERAGE).sort())
115130
})

apps/sim/lib/integrations/icon-mapping.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
462462
okta: OktaIcon,
463463
onedrive: MicrosoftOneDriveIcon,
464464
onepassword: OnePasswordIcon,
465+
oracle_fusion_financials: NetSuiteIcon,
465466
outlook: OutlookIcon,
466467
pagerduty: PagerDutyIcon,
467468
parallel_ai: ParallelIcon,
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { beforeEach, describe, expect, it, vi } from 'vitest'
5+
6+
const { mockRequest } = vi.hoisted(() => ({ mockRequest: vi.fn() }))
7+
vi.mock('@/lib/internal/oracle-fusion/client', () => ({
8+
requestOracleFusionJson: mockRequest,
9+
}))
10+
11+
import type { OracleFusionRequest } from '@/lib/internal/oracle-fusion/client'
12+
import { OracleFusionProviderError } from '@/lib/internal/oracle-fusion/errors'
13+
import { executeOracleFusionFinancialsTool } from '@/lib/internal/oracle-fusion-financials/execute-tool'
14+
import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types'
15+
import { OracleFusionFinancialsBlock } from '@/blocks/blocks/oracle_fusion_financials'
16+
import * as financialsTools from '@/tools/oracle_fusion_financials'
17+
18+
const ORIGIN = 'https://vision.fa.us2.oraclecloud.com'
19+
const AUTH = {
20+
oauthCredential: 'credential-1',
21+
accessToken: 'secret-credential-canary',
22+
instanceUrl: ORIGIN,
23+
}
24+
25+
function call(overrides: Partial<InternalToolOperationCall> = {}): InternalToolOperationCall {
26+
return {
27+
toolId: 'oracle_fusion_financials_list_payables_invoices',
28+
input: AUTH,
29+
headers: new Headers(),
30+
context: { workflowId: 'workflow-1', userId: 'user-1', workspaceId: 'workspace-1' },
31+
requestId: 'request-1',
32+
...overrides,
33+
}
34+
}
35+
36+
describe('Oracle Fusion Financials execution boundary', () => {
37+
beforeEach(() => {
38+
mockRequest.mockReset()
39+
mockRequest.mockImplementation((_credential: unknown, request: OracleFusionRequest) => {
40+
if (request.query?.limit !== undefined) {
41+
return { items: [], count: 0, hasMore: false, limit: 50, offset: 0 }
42+
}
43+
return {
44+
'@context': {
45+
links: [
46+
{
47+
rel: 'self',
48+
href: `${ORIGIN}/fscmRestApi/resources/11.13.18.05/${request.address.relativePath}`,
49+
},
50+
],
51+
},
52+
accessToken: AUTH.accessToken,
53+
}
54+
})
55+
})
56+
57+
it.each(Object.values(financialsTools).filter((tool) =>
58+
/_payables_|_payment_process_request/.test(tool.id)
59+
))(
60+
'executes the real $id declaration and operation',
61+
async (tool) => {
62+
const params = {
63+
...AUTH,
64+
invoiceUniqId: 'invoice-key',
65+
invoiceLineUniqId: 'line-key',
66+
invoiceInstallmentUniqId: 'installment-key',
67+
invoiceDistributionId: '99',
68+
appliedPrepaymentUniqId: 'applied-key',
69+
availablePrepaymentUniqId: 'available-key',
70+
checkId: '42',
71+
invoicePaymentId: '88',
72+
holdId: '21',
73+
paymentProcessRequestId: '17',
74+
termsId: '73',
75+
paymentTermLineUniqId: 'term-line-key',
76+
}
77+
const input = tool.operation.input(params)
78+
const response = await executeOracleFusionFinancialsTool(call({ toolId: tool.id, input }))
79+
expect(response.status).toBe(200)
80+
const result = await response.json()
81+
expect(result.success).toBe(true)
82+
expect(mockRequest).toHaveBeenCalledTimes(1)
83+
expect(JSON.stringify(result)).not.toMatch(/secret-credential-canary|accessToken|instanceUrl/)
84+
}
85+
)
86+
87+
it('distinguishes invalid inputs from malformed provider payloads without reflecting either', async () => {
88+
const invalid = await executeOracleFusionFinancialsTool(
89+
call({ input: { ...AUTH, limit: 101 } })
90+
)
91+
expect(invalid.status).toBe(400)
92+
await expect(invalid.json()).resolves.toEqual({
93+
success: false,
94+
output: {},
95+
error: 'Invalid Oracle Fusion Financials input',
96+
})
97+
expect(mockRequest).not.toHaveBeenCalled()
98+
99+
mockRequest.mockResolvedValue({ items: [AUTH], count: 'secret-provider-value' })
100+
const malformed = await executeOracleFusionFinancialsTool(call())
101+
expect(malformed.status).toBe(502)
102+
await expect(malformed.json()).resolves.toEqual({
103+
success: false,
104+
output: {},
105+
error: 'Oracle Fusion Financials returned an unexpected response shape',
106+
})
107+
})
108+
109+
it('preserves the safe shared provider error and hides unexpected internal failures', async () => {
110+
mockRequest.mockRejectedValueOnce(
111+
new OracleFusionProviderError('Oracle Fusion request failed', 403)
112+
)
113+
const provider = await executeOracleFusionFinancialsTool(call())
114+
expect(provider.status).toBe(403)
115+
await expect(provider.json()).resolves.toMatchObject({ error: 'Oracle Fusion request failed' })
116+
117+
mockRequest.mockRejectedValueOnce(new Error(AUTH.accessToken))
118+
const internal = await executeOracleFusionFinancialsTool(call())
119+
expect(internal.status).toBe(500)
120+
await expect(internal.json()).resolves.toEqual({
121+
success: false,
122+
output: {},
123+
error: 'Oracle Fusion Financials request failed',
124+
})
125+
})
126+
127+
it('forwards cancellation and never reports an aborted request as an ordinary failure', async () => {
128+
const controller = new AbortController()
129+
const reason = new Error('cancelled')
130+
mockRequest.mockImplementationOnce((_auth, _request, signal: AbortSignal) => {
131+
expect(signal).toBe(controller.signal)
132+
controller.abort(reason)
133+
throw reason
134+
})
135+
await expect(
136+
executeOracleFusionFinancialsTool(call({ signal: controller.signal }))
137+
).rejects.toBe(reason)
138+
mockRequest.mockClear()
139+
await expect(
140+
executeOracleFusionFinancialsTool(call({ signal: controller.signal }))
141+
).rejects.toBe(reason)
142+
expect(mockRequest).not.toHaveBeenCalled()
143+
})
144+
145+
it('rejects unsupported operations without sending a provider request', async () => {
146+
const result = await executeOracleFusionFinancialsTool(
147+
call({ toolId: 'oracle_fusion_financials_delete_invoice' })
148+
)
149+
expect(result.status).toBe(500)
150+
expect(mockRequest).not.toHaveBeenCalled()
151+
})
152+
153+
it('coerces block controls only at execution and preserves opaque manual keys', () => {
154+
const config = OracleFusionFinancialsBlock.tools.config!
155+
const params = {
156+
operation: 'oracle_fusion_financials_list_payables_invoice_lines',
157+
invoiceUniqId: ' opaque%2Fkey ',
158+
limit: '25',
159+
offset: '50',
160+
totalResults: 'true',
161+
}
162+
expect(config.tool(params)).toBe(params.operation)
163+
expect(config.params!(params)).toMatchObject({
164+
invoiceUniqId: ' opaque%2Fkey ',
165+
limit: 25,
166+
offset: 50,
167+
totalResults: true,
168+
})
169+
expect(config.tool({ ...params, limit: 'invalid' })).toBe(params.operation)
170+
expect(() => config.params!({ ...params, limit: 'invalid' })).toThrow()
171+
})
172+
173+
it('coerces write inputs only at execution without rounding identifiers or losing explicit nulls', () => {
174+
const config = OracleFusionFinancialsBlock.tools.config!
175+
const params = {
176+
operation: 'oracle_fusion_financials_apply_receivables_receipt',
177+
receivablesReceiptId: '42',
178+
appliedPaymentScheduleId: '9007199254740993',
179+
amountApplied: '12.5',
180+
}
181+
expect(config.tool({ ...params, amountApplied: 'invalid' })).toBe(params.operation)
182+
expect(config.params!(params)).toMatchObject({
183+
appliedPaymentScheduleId: '9007199254740993', amountApplied: 12.5,
184+
})
185+
expect(() => config.params!({ ...params, amountApplied: 'invalid' })).toThrow()
186+
expect(config.params!({
187+
operation: 'oracle_fusion_financials_update_receivables_receipt',
188+
receivablesReceiptId: '42', conversionRate: null,
189+
})).toMatchObject({ conversionRate: null })
190+
expect(config.params!({
191+
operation: 'oracle_fusion_financials_create_receivables_invoice',
192+
lines: '[{"LineNumber":1,"Quantity":2}]',
193+
})).toMatchObject({ lines: [{ LineNumber: 1, Quantity: 2 }] })
194+
})
195+
196+
it('dispatches receipt application and returns a typed business failure without credential data', async () => {
197+
mockRequest.mockResolvedValueOnce({ result: 'ERROR', accessToken: AUTH.accessToken })
198+
const tool = financialsTools.oracleFusionFinancialsApplyReceivablesReceiptTool
199+
const input = tool.operation.input({
200+
...AUTH, receivablesReceiptId: '42', appliedPaymentScheduleId: '9007199254740993',
201+
})
202+
const response = await executeOracleFusionFinancialsTool(call({ toolId: tool.id, input }))
203+
expect(response.status).toBe(200)
204+
await expect(response.json()).resolves.toEqual({
205+
success: false,
206+
output: { result: 'ERROR' },
207+
error: 'Oracle Fusion action reported an unsuccessful result',
208+
})
209+
})
210+
})
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ZodError } from 'zod'
2+
import { OracleFusionProviderError } from '@/lib/internal/oracle-fusion/errors'
3+
import {
4+
executeOracleFusionFinancialsOperation,
5+
isOracleFusionFinancialsToolId,
6+
} from '@/lib/internal/oracle-fusion-financials/operations'
7+
import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types'
8+
9+
export const executeOracleFusionFinancialsTool: InternalToolOperationHandler = async ({
10+
toolId,
11+
input,
12+
signal,
13+
}) => {
14+
signal?.throwIfAborted()
15+
if (!isOracleFusionFinancialsToolId(toolId)) {
16+
return Response.json(
17+
{ success: false, error: `Unsupported Oracle Fusion Financials tool: ${toolId}` },
18+
{ status: 500 }
19+
)
20+
}
21+
22+
try {
23+
return Response.json(await executeOracleFusionFinancialsOperation(toolId, input, signal))
24+
} catch (error) {
25+
signal?.throwIfAborted()
26+
if (error instanceof OracleFusionProviderError) {
27+
return Response.json(
28+
{ success: false, output: {}, error: error.message },
29+
{ status: error.status }
30+
)
31+
}
32+
const validationFailure = error instanceof ZodError
33+
return Response.json(
34+
{
35+
success: false,
36+
output: {},
37+
error: validationFailure
38+
? 'Invalid Oracle Fusion Financials input'
39+
: 'Oracle Fusion Financials request failed',
40+
},
41+
{ status: validationFailure ? 400 : 500 }
42+
)
43+
}
44+
}

0 commit comments

Comments
 (0)