Skip to content
Open
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_hcm: 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_hcm",
"outlook",
"pagerduty",
"parallel_ai",
Expand Down
2,813 changes: 2,813 additions & 0 deletions apps/docs/content/docs/integrations/oracle_fusion_hcm.mdx

Large diffs are not rendered by default.

2,780 changes: 2,780 additions & 0 deletions apps/sim/blocks/blocks/oracle_fusion_hcm.ts

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions apps/sim/blocks/registry-maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ 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 { OracleFusionHcmBlock, OracleFusionHcmBlockMeta } from '@/blocks/blocks/oracle_fusion_hcm'
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 +594,7 @@ export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
onedrive: OneDriveBlock,
onepassword: OnePasswordBlock,
openai: OpenAIBlock,
oracle_fusion_hcm: OracleFusionHcmBlock,
outlook: OutlookBlock,
pagerduty: PagerDutyBlock,
parallel_ai: ParallelBlock,
Expand Down Expand Up @@ -914,6 +916,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
onedrive: OneDriveBlockMeta,
onepassword: OnePasswordBlockMeta,
openai: OpenAIBlockMeta,
oracle_fusion_hcm: OracleFusionHcmBlockMeta,
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_hcm.mdx',
'integrations/outlook.mdx',
'integrations/pagerduty.mdx',
'integrations/parallel_ai.mdx',
Expand Down
2 changes: 2 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,8 @@ 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': [],
// Fusion HCM also uses reusable credentials on an API-key catalog block.
'oracle-fusion-service-account': [],
'pipedrive-service-account': ['pipedrive'],
'salesforce-service-account': ['salesforce'],
'shopify-service-account': ['shopify'],
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_hcm: NetSuiteIcon,
outlook: OutlookIcon,
pagerduty: PagerDutyIcon,
parallel_ai: ParallelIcon,
Expand Down
239 changes: 239 additions & 0 deletions apps/sim/lib/internal/oracle-fusion-hcm/execute-tool.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { OracleFusionProviderError } from '@/lib/internal/oracle-fusion/errors'
import { executeOracleFusionHcmTool } from '@/lib/internal/oracle-fusion-hcm/execute-tool'
import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types'

const TEST_ACCESS_TOKEN = 'test-access-token'

const mocks = vi.hoisted(() => ({
createAssignedPayroll: vi.fn(),
updateAssignedPayroll: vi.fn(),
createElementEntry: vi.fn(),
updateElementEntryValue: vi.fn(),
createSalary: vi.fn(),
correctSalary: vi.fn(),
createTimeEntry: vi.fn(),
updateTimeEntry: vi.fn(),
deleteTimeEntry: vi.fn(),
listWorkers: vi.fn(),
getWorker: vi.fn(),
listWorkerAssignments: vi.fn(),
getWorkerAssignment: vi.fn(),
listWorkerManagers: vi.fn(),
listWorkerDirectReports: vi.fn(),
listAbsences: vi.fn(),
getAbsence: vi.fn(),
listAbsenceTypes: vi.fn(),
listJobs: vi.fn(),
listJobFamilies: vi.fn(),
listDepartments: vi.fn(),
listLocations: vi.fn(),
listPositions: vi.fn(),
listBusinessUnits: vi.fn(),
listLegalEmployers: vi.fn(),
listGrades: vi.fn(),
listPersonTypes: vi.fn(),
}))

vi.mock('@/lib/internal/oracle-fusion-hcm/operations', () => ({
executeOracleFusionHcmCreateAssignedPayroll: mocks.createAssignedPayroll,
executeOracleFusionHcmUpdateAssignedPayroll: mocks.updateAssignedPayroll,
executeOracleFusionHcmCreateElementEntry: mocks.createElementEntry,
executeOracleFusionHcmUpdateElementEntryValue: mocks.updateElementEntryValue,
executeOracleFusionHcmCreateSalary: mocks.createSalary,
executeOracleFusionHcmCorrectSalary: mocks.correctSalary,
executeOracleFusionHcmCreateTimeEntry: mocks.createTimeEntry,
executeOracleFusionHcmUpdateTimeEntry: mocks.updateTimeEntry,
executeOracleFusionHcmDeleteTimeEntry: mocks.deleteTimeEntry,
executeOracleFusionHcmListWorkers: mocks.listWorkers,
executeOracleFusionHcmGetWorker: mocks.getWorker,
executeOracleFusionHcmListWorkerAssignments: mocks.listWorkerAssignments,
executeOracleFusionHcmGetWorkerAssignment: mocks.getWorkerAssignment,
executeOracleFusionHcmListWorkerManagers: mocks.listWorkerManagers,
executeOracleFusionHcmListWorkerDirectReports: mocks.listWorkerDirectReports,
executeOracleFusionHcmListAbsences: mocks.listAbsences,
executeOracleFusionHcmGetAbsence: mocks.getAbsence,
executeOracleFusionHcmListAbsenceTypes: mocks.listAbsenceTypes,
executeOracleFusionHcmListJobs: mocks.listJobs,
executeOracleFusionHcmListJobFamilies: mocks.listJobFamilies,
executeOracleFusionHcmListDepartments: mocks.listDepartments,
executeOracleFusionHcmListLocations: mocks.listLocations,
executeOracleFusionHcmListPositions: mocks.listPositions,
executeOracleFusionHcmListBusinessUnits: mocks.listBusinessUnits,
executeOracleFusionHcmListLegalEmployers: mocks.listLegalEmployers,
executeOracleFusionHcmListGrades: mocks.listGrades,
executeOracleFusionHcmListPersonTypes: mocks.listPersonTypes,
}))

const auth = {
instanceUrl: 'https://acme.fa.ocs.oraclecloud.com',
accessToken: TEST_ACCESS_TOKEN,
}

function invokeHcmTool(overrides: Partial<InternalToolOperationCall>) {
return executeOracleFusionHcmTool({
toolId: 'oracle_fusion_hcm_list_workers',
input: auth,
headers: new Headers({ 'content-type': 'application/json' }),
context: { workflowId: 'workflow-1', workspaceId: 'workspace-1', userId: 'user-1' },
requestId: 'request-1',
...overrides,
})
}

describe('Oracle Fusion HCM tool dispatch', () => {
it('turns an explicit model clear flag into a nullable element value before execution', async () => {
const response = await invokeHcmTool({
toolId: 'oracle_fusion_hcm_update_element_entry_value',
input: {
...auth,
elementEntryId: '1',
elementEntryValueId: '2',
effectiveDate: '2026-01-01',
rangeMode: 'CORRECTION',
clearScreenEntryValue: true,
},
})
expect(response.status).toBe(200)
expect(mocks.updateElementEntryValue).toHaveBeenCalledWith(
expect.objectContaining({ screenEntryValue: null }),
undefined
)
})

it('marks ambiguous mutation failures non-retryable without exposing provider details', async () => {
mocks.createSalary.mockRejectedValueOnce(
new OracleFusionProviderError('Oracle Fusion HCM request timed out', 504)
)
const response = await invokeHcmTool({
toolId: 'oracle_fusion_hcm_create_salary',
input: {
...auth,
assignmentId: '1',
salaryBasisId: '2',
salaryAmount: 1200,
dateFrom: '2026-01-01',
dateTo: '4712-12-31',
},
})
expect(response.status).toBe(504)
expect(await response.json()).toEqual({
success: false,
error: 'Oracle Fusion HCM request timed out',
retryable: false,
})
expect(mocks.createSalary).toHaveBeenCalledOnce()
})

it('rejects a time update missing its version before dispatch', async () => {
const response = await invokeHcmTool({
toolId: 'oracle_fusion_hcm_update_time_entry',
input: {
...auth,
personNumber: '0007',
timeRecordId: '2',
measure: 8,
referenceDate: '2026-01-01',
},
})
expect(response.status).toBe(400)
expect(mocks.updateTimeEntry).not.toHaveBeenCalled()
})
beforeEach(() => {
vi.clearAllMocks()
for (const mock of Object.values(mocks)) {
mock.mockResolvedValue({ success: true, output: {} })
}
})

it.each([
['oracle_fusion_hcm_list_workers', mocks.listWorkers, {}],
['oracle_fusion_hcm_get_worker', mocks.getWorker, { personId: '1' }],
['oracle_fusion_hcm_list_worker_assignments', mocks.listWorkerAssignments, { personId: '1' }],
[
'oracle_fusion_hcm_get_worker_assignment',
mocks.getWorkerAssignment,
{ personId: '1', assignmentId: '2' },
],
[
'oracle_fusion_hcm_list_worker_managers',
mocks.listWorkerManagers,
{ personId: '1', assignmentId: '2' },
],
[
'oracle_fusion_hcm_list_worker_direct_reports',
mocks.listWorkerDirectReports,
{ personId: '1', assignmentId: '2' },
],
['oracle_fusion_hcm_list_absences', mocks.listAbsences, { personId: '1' }],
['oracle_fusion_hcm_get_absence', mocks.getAbsence, { absenceId: '3' }],
['oracle_fusion_hcm_list_absence_types', mocks.listAbsenceTypes, { personId: '1' }],
['oracle_fusion_hcm_list_jobs', mocks.listJobs, {}],
['oracle_fusion_hcm_list_job_families', mocks.listJobFamilies, {}],
['oracle_fusion_hcm_list_departments', mocks.listDepartments, {}],
['oracle_fusion_hcm_list_locations', mocks.listLocations, {}],
['oracle_fusion_hcm_list_positions', mocks.listPositions, {}],
['oracle_fusion_hcm_list_business_units', mocks.listBusinessUnits, {}],
['oracle_fusion_hcm_list_legal_employers', mocks.listLegalEmployers, {}],
['oracle_fusion_hcm_list_grades', mocks.listGrades, {}],
['oracle_fusion_hcm_list_person_types', mocks.listPersonTypes, {}],
])('dispatches %s', async (toolId, operation, extra) => {
const response = await invokeHcmTool({ toolId, input: { ...auth, ...extra } })
expect(response.status).toBe(200)
expect(operation).toHaveBeenCalledOnce()
})

it('rejects cross-field absence validation before dispatch', async () => {
const response = await invokeHcmTool({
toolId: 'oracle_fusion_hcm_list_absences',
input: { ...auth, personId: '1', startDate: '2026-01-01' },
})
expect(response.status).toBe(400)
expect(mocks.listAbsences).not.toHaveBeenCalled()
})

it('propagates cancellation that occurs while an operation resolves', async () => {
const controller = new AbortController()
mocks.listWorkers.mockImplementationOnce(async () => {
controller.abort(new Error('caller stopped'))
return { success: true, output: {} }
})

await expect(
invokeHcmTool({
toolId: 'oracle_fusion_hcm_list_workers',
input: auth,
signal: controller.signal,
})
).rejects.toThrow('caller stopped')
})

it('maps operation errors and rejects unknown tools', async () => {
mocks.getWorker.mockRejectedValueOnce(new OracleFusionProviderError('denied', 403))
const denied = await invokeHcmTool({
toolId: 'oracle_fusion_hcm_get_worker',
input: { ...auth, personId: '1' },
})
expect(denied.status).toBe(403)
expect(await denied.json()).toEqual({ success: false, error: 'denied' })

const unknown = await invokeHcmTool({
toolId: 'oracle_fusion_hcm_nope',
input: auth,
})
expect(unknown.status).toBe(500)
})

it('does not reflect unexpected error messages', async () => {
mocks.listWorkers.mockRejectedValueOnce(new Error('secret provider detail'))
const response = await invokeHcmTool({
toolId: 'oracle_fusion_hcm_list_workers',
input: auth,
})
expect(response.status).toBe(500)
expect(await response.json()).toEqual({
success: false,
error: 'Oracle Fusion HCM request failed',
})
})
})
Loading
Loading