Skip to content

Commit 00bb6df

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

138 files changed

Lines changed: 26138 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: 2447 additions & 0 deletions
Large diffs are not rendered by default.

apps/sim/blocks/blocks/oracle_fusion_financials.ts

Lines changed: 6049 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: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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(
58+
Object.values(financialsTools).filter((tool) =>
59+
/_payables_|_payment_process_request/.test(tool.id)
60+
)
61+
)('executes the real $id declaration and operation', 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+
it('distinguishes invalid inputs from malformed provider payloads without reflecting either', async () => {
87+
const invalid = await executeOracleFusionFinancialsTool(
88+
call({ input: { ...AUTH, limit: 101 } })
89+
)
90+
expect(invalid.status).toBe(400)
91+
await expect(invalid.json()).resolves.toEqual({
92+
success: false,
93+
output: {},
94+
error: 'Invalid Oracle Fusion Financials input',
95+
})
96+
expect(mockRequest).not.toHaveBeenCalled()
97+
98+
mockRequest.mockResolvedValue({ items: [AUTH], count: 'secret-provider-value' })
99+
const malformed = await executeOracleFusionFinancialsTool(call())
100+
expect(malformed.status).toBe(502)
101+
await expect(malformed.json()).resolves.toEqual({
102+
success: false,
103+
output: {},
104+
error: 'Oracle Fusion Financials returned an unexpected response shape',
105+
})
106+
})
107+
108+
it('preserves the safe shared provider error and hides unexpected internal failures', async () => {
109+
mockRequest.mockRejectedValueOnce(
110+
new OracleFusionProviderError('Oracle Fusion request failed', 403)
111+
)
112+
const provider = await executeOracleFusionFinancialsTool(call())
113+
expect(provider.status).toBe(403)
114+
await expect(provider.json()).resolves.toMatchObject({ error: 'Oracle Fusion request failed' })
115+
116+
mockRequest.mockRejectedValueOnce(new Error(AUTH.accessToken))
117+
const internal = await executeOracleFusionFinancialsTool(call())
118+
expect(internal.status).toBe(500)
119+
await expect(internal.json()).resolves.toEqual({
120+
success: false,
121+
output: {},
122+
error: 'Oracle Fusion Financials request failed',
123+
})
124+
})
125+
126+
it('forwards cancellation and never reports an aborted request as an ordinary failure', async () => {
127+
const controller = new AbortController()
128+
const reason = new Error('cancelled')
129+
mockRequest.mockImplementationOnce((_auth, _request, signal: AbortSignal) => {
130+
expect(signal).toBe(controller.signal)
131+
controller.abort(reason)
132+
throw reason
133+
})
134+
await expect(
135+
executeOracleFusionFinancialsTool(call({ signal: controller.signal }))
136+
).rejects.toBe(reason)
137+
mockRequest.mockClear()
138+
await expect(
139+
executeOracleFusionFinancialsTool(call({ signal: controller.signal }))
140+
).rejects.toBe(reason)
141+
expect(mockRequest).not.toHaveBeenCalled()
142+
})
143+
144+
it('rejects unsupported operations without sending a provider request', async () => {
145+
const result = await executeOracleFusionFinancialsTool(
146+
call({ toolId: 'oracle_fusion_financials_delete_invoice' })
147+
)
148+
expect(result.status).toBe(500)
149+
expect(mockRequest).not.toHaveBeenCalled()
150+
})
151+
152+
it('coerces block controls only at execution and preserves opaque manual keys', () => {
153+
const config = OracleFusionFinancialsBlock.tools.config!
154+
const params = {
155+
operation: 'oracle_fusion_financials_list_payables_invoice_lines',
156+
invoiceUniqId: ' opaque%2Fkey ',
157+
limit: '25',
158+
offset: '50',
159+
totalResults: 'true',
160+
}
161+
expect(config.tool(params)).toBe(params.operation)
162+
expect(config.params!(params)).toMatchObject({
163+
invoiceUniqId: ' opaque%2Fkey ',
164+
limit: 25,
165+
offset: 50,
166+
totalResults: true,
167+
})
168+
expect(config.tool({ ...params, limit: 'invalid' })).toBe(params.operation)
169+
expect(() => config.params!({ ...params, limit: 'invalid' })).toThrow()
170+
})
171+
172+
it('coerces write inputs only at execution without rounding identifiers or losing explicit nulls', () => {
173+
const config = OracleFusionFinancialsBlock.tools.config!
174+
const params = {
175+
operation: 'oracle_fusion_financials_apply_receivables_receipt',
176+
receivablesReceiptId: '42',
177+
appliedPaymentScheduleId: '9007199254740993',
178+
amountApplied: '12.5',
179+
}
180+
expect(config.tool({ ...params, amountApplied: 'invalid' })).toBe(params.operation)
181+
expect(config.params!(params)).toMatchObject({
182+
appliedPaymentScheduleId: '9007199254740993',
183+
amountApplied: 12.5,
184+
})
185+
expect(() => config.params!({ ...params, amountApplied: 'invalid' })).toThrow()
186+
expect(
187+
config.params!({
188+
operation: 'oracle_fusion_financials_update_receivables_receipt',
189+
receivablesReceiptId: '42',
190+
conversionRate: null,
191+
})
192+
).toMatchObject({ conversionRate: null })
193+
expect(
194+
config.params!({
195+
operation: 'oracle_fusion_financials_create_receivables_invoice',
196+
lines: '[{"LineNumber":1,"Quantity":2}]',
197+
})
198+
).toMatchObject({ lines: [{ LineNumber: 1, Quantity: 2 }] })
199+
})
200+
201+
it('dispatches receipt application and returns a typed business failure without credential data', async () => {
202+
mockRequest.mockResolvedValueOnce({ result: 'ERROR', accessToken: AUTH.accessToken })
203+
const tool = financialsTools.oracleFusionFinancialsApplyReceivablesReceiptTool
204+
const input = tool.operation.input({
205+
...AUTH,
206+
receivablesReceiptId: '42',
207+
appliedPaymentScheduleId: '9007199254740993',
208+
})
209+
const response = await executeOracleFusionFinancialsTool(call({ toolId: tool.id, input }))
210+
expect(response.status).toBe(200)
211+
await expect(response.json()).resolves.toEqual({
212+
success: false,
213+
output: { result: 'ERROR' },
214+
error: 'Oracle Fusion action reported an unsuccessful result',
215+
})
216+
})
217+
218+
it('dispatches report submission and preserves submission errors as a business failure', async () => {
219+
const controller = new AbortController()
220+
mockRequest.mockResolvedValueOnce({ result: '1:EXP-42' })
221+
const tool = financialsTools.oracleFusionFinancialsSubmitExpenseReportTool
222+
const input = tool.operation.input({ ...AUTH, expenseReportUniqId: ' report key ' })
223+
const response = await executeOracleFusionFinancialsTool(
224+
call({ toolId: tool.id, input, signal: controller.signal })
225+
)
226+
expect(response.status).toBe(200)
227+
await expect(response.json()).resolves.toMatchObject({
228+
success: false,
229+
output: { result: '1:EXP-42' },
230+
})
231+
expect(mockRequest).toHaveBeenCalledWith(
232+
expect.objectContaining(AUTH),
233+
expect.objectContaining({
234+
address: {
235+
family: 'fscm',
236+
relativePath: 'expenseReports/%20report%20key%20/action/submit',
237+
},
238+
method: 'POST',
239+
}),
240+
controller.signal
241+
)
242+
})
243+
244+
it('dispatches ledger balances with execution-time pagination and unchanged finder text', async () => {
245+
const tool = financialsTools.oracleFusionFinancialsListGlBalancesTool
246+
const config = OracleFusionFinancialsBlock.tools.config!
247+
const finder = 'AccountGroupBalanceFinder;accountGroupName=Cash,ledgerName=US Primary'
248+
const params = config.params!({
249+
...AUTH,
250+
operation: tool.id,
251+
finder,
252+
limit: '25',
253+
offset: '50',
254+
})
255+
mockRequest.mockResolvedValueOnce({
256+
items: [{ EndingBalance: '#MISSING' }],
257+
count: 1,
258+
hasMore: false,
259+
limit: 25,
260+
offset: 50,
261+
})
262+
const response = await executeOracleFusionFinancialsTool(
263+
call({ toolId: tool.id, input: tool.operation.input(params) })
264+
)
265+
await expect(response.json()).resolves.toMatchObject({
266+
success: true,
267+
output: { items: [{ EndingBalance: '#MISSING' }], limit: 25, offset: 50 },
268+
})
269+
expect(mockRequest).toHaveBeenCalledWith(
270+
expect.objectContaining(AUTH),
271+
expect.objectContaining({
272+
address: { family: 'fscm', relativePath: 'ledgerBalances' },
273+
query: expect.objectContaining({ finder, limit: 25, offset: 50 }),
274+
}),
275+
undefined
276+
)
277+
})
278+
})

0 commit comments

Comments
 (0)