Skip to content

Commit 7864589

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(oracle-fusion): preserve int64 identifiers
1 parent d9cf784 commit 7864589

5 files changed

Lines changed: 143 additions & 48 deletions

File tree

apps/sim/lib/internal/oracle-fusion-financials/client.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,32 @@ const REQUEST_TIMEOUT_MS = 30_000
1515
const RESPONSE_MAX_BYTES = 5 * 1024 * 1024
1616
const MAX_RETRIES = 2
1717
const TRANSIENT_STATUSES = new Set([429, 503, 504])
18+
const LOSSLESS_DECIMAL_FIELDS = new Set([
19+
'InvoiceId',
20+
'InvoiceDistributionId',
21+
'CheckId',
22+
'PaymentId',
23+
'PaymentReference',
24+
'PaymentNumber',
25+
'InvoicePaymentId',
26+
'HoldId',
27+
'PaymentProcessRequestId',
28+
'SourceApplicationIdentifier',
29+
'termsId',
30+
'setId',
31+
])
32+
const DECIMAL_INTEGER_TOKEN = /^\d+$/
33+
34+
interface JsonParseContext {
35+
source?: string
36+
}
37+
38+
type JsonParseWithSource = (
39+
text: string,
40+
reviver: (this: unknown, key: string, value: unknown, context?: JsonParseContext) => unknown
41+
) => unknown
42+
43+
const jsonParseWithSource = JSON.parse as JsonParseWithSource
1844

1945
export class OracleFusionFinancialsProviderError extends Error {
2046
constructor(
@@ -60,6 +86,15 @@ function sanitizeOracleError(body: string, accessToken: string, status: number):
6086
return safe || `Oracle Fusion Financials request failed with HTTP ${status}`
6187
}
6288

89+
/** Keeps Oracle int64 identifiers exact while leaving monetary and counter fields numeric. */
90+
function parseOracleFusionJson(body: string): unknown {
91+
return jsonParseWithSource(body, (key, value, context) => {
92+
if (!LOSSLESS_DECIMAL_FIELDS.has(key) || typeof value !== 'number') return value
93+
const source = context?.source
94+
return source && DECIMAL_INTEGER_TOKEN.test(source) ? source : value
95+
})
96+
}
97+
6398
export interface OracleFusionRequest {
6499
path: string
65100
query?: Record<string, string | number | boolean | undefined>
@@ -153,7 +188,7 @@ export async function requestOracleFusionJson(
153188
)
154189
}
155190
try {
156-
return JSON.parse(body) as unknown
191+
return parseOracleFusionJson(body)
157192
} catch {
158193
throw new OracleFusionFinancialsProviderError(
159194
'Oracle Fusion Financials returned a malformed JSON response',

apps/sim/lib/internal/oracle-fusion-financials/oracle-fusion-financials.test.ts

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,20 @@ const BOOLEAN_FIELDS = new Set([
366366
'ReversedFlag',
367367
'TrackAsAssetFlag',
368368
])
369+
const DECIMAL_STRING_FIELDS = new Set([
370+
'InvoiceId',
371+
'InvoiceDistributionId',
372+
'CheckId',
373+
'PaymentId',
374+
'PaymentReference',
375+
'PaymentNumber',
376+
'InvoicePaymentId',
377+
'HoldId',
378+
'PaymentProcessRequestId',
379+
'SourceApplicationIdentifier',
380+
'termsId',
381+
'setId',
382+
])
369383
const NUMBER_FIELDS = new Set([
370384
'AmountPaid',
371385
'AmountPaidInvoiceCurrency',
@@ -374,7 +388,6 @@ const NUMBER_FIELDS = new Set([
374388
'AppliedAmount',
375389
'AvailableAmount',
376390
'BaseAmount',
377-
'CheckId',
378391
'CrossCurrencyRate',
379392
'cutoffDay',
380393
'dayOfMonth',
@@ -390,26 +403,18 @@ const NUMBER_FIELDS = new Set([
390403
'firstDiscountPercent',
391404
'FirstDiscountAmount',
392405
'GrossAmount',
393-
'HoldId',
394406
'IncludedTax',
395407
'InstallmentNumber',
396408
'InvoiceAmount',
397409
'InvoiceBaseAmount',
398-
'InvoiceDistributionId',
399-
'InvoiceId',
400410
'InvoicePaymentAmount',
401-
'InvoicePaymentId',
402411
'LineAmount',
403412
'LineHeld',
404413
'LineNumber',
405414
'monthsAhead',
406415
'PaymentAmount',
407416
'PaymentBaseAmount',
408-
'PaymentId',
409-
'PaymentNumber',
410417
'PaymentPriority',
411-
'PaymentProcessRequestId',
412-
'PaymentReference',
413418
'PurchaseOrderDistributionLineNumber',
414419
'PurchaseOrderLineNumber',
415420
'PurchaseOrderScheduleLineNumber',
@@ -422,9 +427,6 @@ const NUMBER_FIELDS = new Set([
422427
'secondDiscountPercent',
423428
'SecondDiscountAmount',
424429
'sequenceNumber',
425-
'setId',
426-
'SourceApplicationIdentifier',
427-
'termsId',
428430
'thirdDiscountDayOfMonth',
429431
'thirdDiscountDays',
430432
'thirdDiscountMonthsForward',
@@ -548,7 +550,13 @@ function documentedFixture(fields: readonly string[]): Record<string, unknown> {
548550
return Object.fromEntries(
549551
fields.map((field) => [
550552
field,
551-
BOOLEAN_FIELDS.has(field) ? true : NUMBER_FIELDS.has(field) ? 1 : 'value',
553+
BOOLEAN_FIELDS.has(field)
554+
? true
555+
: DECIMAL_STRING_FIELDS.has(field)
556+
? '1'
557+
: NUMBER_FIELDS.has(field)
558+
? 1
559+
: 'value',
552560
])
553561
)
554562
}
@@ -771,7 +779,7 @@ describe('Oracle Fusion Financials provider', () => {
771779
)
772780

773781
it.each(RESOURCE_SCHEMA_CASES)(
774-
'accepts documented scalar types and nullable values for the %s projection',
782+
'accepts projected scalar types and documented nullable values for the %s projection',
775783
(_name, schema, fields, nonNullableFields) => {
776784
expect(schema.parse(documentedFixture(fields))).toMatchObject(documentedFixture(fields))
777785
const nullableFixture = Object.fromEntries(
@@ -801,6 +809,16 @@ describe('Oracle Fusion Financials provider', () => {
801809
}
802810
)
803811

812+
it('publishes lossless Oracle identity and reference fields as decimal strings', () => {
813+
for (const [fields, properties] of RESOURCE_OUTPUT_CASES) {
814+
for (const field of fields) {
815+
if (DECIMAL_STRING_FIELDS.has(field)) {
816+
expect(properties[field], field).toMatchObject({ type: 'string' })
817+
}
818+
}
819+
}
820+
})
821+
804822
it.each(RESOURCE_SCHEMA_CASES)(
805823
'rejects the wrong scalar type for every %s projection field',
806824
(_name, schema, fields) => {
@@ -989,12 +1007,46 @@ describe('Oracle Fusion Financials provider', () => {
9891007
})
9901008

9911009
it.each(['PaymentReference', 'PaymentNumber'])(
992-
'rejects a fractional %s even though other payment amounts accept decimals',
1010+
'rejects a non-decimal %s even though other payment amounts accept decimals',
9931011
(field) => {
994-
expect(oracleFusionPaymentSchema.safeParse({ [field]: 1.5 }).success).toBe(false)
1012+
expect(oracleFusionPaymentSchema.safeParse({ [field]: '1.5' }).success).toBe(false)
1013+
expect(oracleFusionPaymentSchema.safeParse({ [field]: 1 }).success).toBe(false)
9951014
}
9961015
)
9971016

1017+
it('preserves Oracle int64 identity tokens exactly as decimal strings', async () => {
1018+
mockSecureFetch.mockResolvedValueOnce(
1019+
response(
1020+
200,
1021+
`{"items":[{"InvoiceId":9007199254740993,"InvoiceNumber":"INV-1","links":[{"rel":"self","href":"${ORIGIN}${INVOICE_PATH}"}]}],"count":1,"hasMore":false,"limit":50,"offset":0}`
1022+
)
1023+
)
1024+
1025+
const result = await executeOracleFusionFinancialsOperation(
1026+
'oracle_fusion_financials_list_payables_invoices',
1027+
AUTH
1028+
)
1029+
1030+
expect((result.output.items as Array<Record<string, unknown>>)[0]?.InvoiceId).toBe(
1031+
'9007199254740993'
1032+
)
1033+
})
1034+
1035+
it('normalizes every projected Oracle identity token before schema validation', async () => {
1036+
const rawFields = [...DECIMAL_STRING_FIELDS]
1037+
.map((field) => `"${field}":9007199254740993`)
1038+
.join(',')
1039+
mockSecureFetch.mockResolvedValueOnce(response(200, `{${rawFields}}`))
1040+
1041+
const result = (await requestOracleFusionJson(AUTH, {
1042+
path: `${RESOURCE_PATH}/invoices`,
1043+
})) as Record<string, unknown>
1044+
1045+
expect(result).toEqual(
1046+
Object.fromEntries([...DECIMAL_STRING_FIELDS].map((field) => [field, '9007199254740993']))
1047+
)
1048+
})
1049+
9981050
it('maps an oversized or otherwise unreadable response body to a sanitized 502', async () => {
9991051
mockSecureFetch.mockResolvedValueOnce({
10001052
...response(200, {}),

apps/sim/lib/internal/oracle-fusion-financials/schema.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,16 @@ const oracleNonNullableText = z.string().optional()
249249
const oracleNonNullableNumber = z.number().finite().optional()
250250
const oracleNonNullableInteger = z.number().int().finite().optional()
251251
const oracleNonNullableBoolean = z.boolean().optional()
252+
const oracleDecimalString = z.string().regex(/^\d+$/).nullable().optional()
253+
const oracleNonNullableDecimalString = z.string().regex(/^\d+$/).optional()
252254
const linkSchema = z
253255
.object({ rel: z.string().optional(), href: z.string().optional() })
254256
.passthrough()
255257
const linksShape = { links: z.array(linkSchema).optional() }
256258

257259
export const oracleFusionInvoiceSchema = z
258260
.object({
259-
InvoiceId: oracleNonNullableInteger,
261+
InvoiceId: oracleNonNullableDecimalString,
260262
InvoiceNumber: oracleNonNullableText,
261263
Supplier: oracleText,
262264
SupplierNumber: oracleNonNullableText,
@@ -334,7 +336,7 @@ export const oracleFusionInstallmentSchema = z
334336

335337
export const oracleFusionInvoiceDistributionSchema = z
336338
.object({
337-
InvoiceDistributionId: oracleNonNullableInteger,
339+
InvoiceDistributionId: oracleNonNullableDecimalString,
338340
DistributionLineNumber: oracleNonNullableInteger,
339341
DistributionLineType: oracleText,
340342
DistributionAmount: oracleNonNullableNumber,
@@ -397,10 +399,10 @@ export const oracleFusionAvailablePrepaymentSchema = z
397399

398400
export const oracleFusionPaymentSchema = z
399401
.object({
400-
CheckId: oracleNonNullableInteger,
401-
PaymentId: oracleInteger,
402-
PaymentReference: oracleInteger,
403-
PaymentNumber: oracleNonNullableInteger,
402+
CheckId: oracleNonNullableDecimalString,
403+
PaymentId: oracleDecimalString,
404+
PaymentReference: oracleDecimalString,
405+
PaymentNumber: oracleNonNullableDecimalString,
404406
PaymentAmount: oracleNonNullableNumber,
405407
PaymentCurrency: oracleNonNullableText,
406408
PaymentDate: oracleNonNullableText,
@@ -422,9 +424,9 @@ export const oracleFusionPaymentSchema = z
422424

423425
export const oracleFusionPaymentRelatedInvoiceSchema = z
424426
.object({
425-
InvoicePaymentId: oracleNonNullableInteger,
426-
CheckId: oracleNonNullableInteger,
427-
InvoiceId: oracleNonNullableInteger,
427+
InvoicePaymentId: oracleNonNullableDecimalString,
428+
CheckId: oracleNonNullableDecimalString,
429+
InvoiceId: oracleNonNullableDecimalString,
428430
InvoiceBusinessUnit: oracleText,
429431
InvoiceNumber: oracleNonNullableText,
430432
InstallmentNumber: oracleNonNullableInteger,
@@ -447,7 +449,7 @@ export const oracleFusionPaymentRelatedInvoiceSchema = z
447449

448450
export const oracleFusionInvoiceHoldSchema = z
449451
.object({
450-
HoldId: oracleNonNullableInteger,
452+
HoldId: oracleNonNullableDecimalString,
451453
InvoiceNumber: oracleText,
452454
BusinessUnit: oracleText,
453455
Supplier: oracleText,
@@ -475,9 +477,9 @@ export const oracleFusionInvoiceHoldSchema = z
475477

476478
export const oracleFusionPaymentProcessRequestSchema = z
477479
.object({
478-
PaymentProcessRequestId: oracleNonNullableInteger,
480+
PaymentProcessRequestId: oracleNonNullableDecimalString,
479481
PaymentProcessRequestName: oracleNonNullableText,
480-
SourceApplicationIdentifier: oracleNonNullableInteger,
482+
SourceApplicationIdentifier: oracleNonNullableDecimalString,
481483
PaymentProcessRequestStatusCode: oracleNonNullableText,
482484
PaymentProcessRequestStatusMeaning: oracleText,
483485
...linksShape,
@@ -486,15 +488,15 @@ export const oracleFusionPaymentProcessRequestSchema = z
486488

487489
export const oracleFusionPaymentTermSchema = z
488490
.object({
489-
termsId: oracleNonNullableInteger,
491+
termsId: oracleNonNullableDecimalString,
490492
name: oracleNonNullableText,
491493
description: oracleText,
492494
enabledFlag: oracleNonNullableBoolean,
493495
fromDate: oracleNonNullableText,
494496
toDate: oracleText,
495497
cutoffDay: oracleInteger,
496498
rank: oracleInteger,
497-
setId: oracleNonNullableInteger,
499+
setId: oracleNonNullableDecimalString,
498500
creationDate: oracleNonNullableText,
499501
lastUpdateDate: oracleNonNullableText,
500502
...linksShape,
@@ -503,7 +505,7 @@ export const oracleFusionPaymentTermSchema = z
503505

504506
export const oracleFusionPaymentTermLineSchema = z
505507
.object({
506-
termsId: oracleNonNullableInteger,
508+
termsId: oracleNonNullableDecimalString,
507509
sequenceNumber: oracleNonNullableInteger,
508510
amountDue: oracleNumber,
509511
calendar: oracleText,

apps/sim/tools/generated/tool-outputs.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)