@@ -62,17 +62,49 @@ function assertAllowedKeys(
6262 if ( unknownKey ) throw new Error ( `${ fieldName } contains unsupported field "${ unknownKey } "` )
6363}
6464
65- function requiredPositiveNumber ( value : unknown , fieldName : string ) : number {
66- const parsed = typeof value === 'number' ? value : Number ( value )
67- if ( ! Number . isFinite ( parsed ) || parsed <= 0 ) {
68- throw new Error ( `${ fieldName } must be a positive finite number` )
65+ function quickBooksMoneyDecimal ( value : unknown , fieldName : string , requirement : string ) : Decimal {
66+ if ( typeof value !== 'number' && typeof value !== 'string' ) {
67+ throw new Error ( `${ fieldName } must be a ${ requirement } ` )
6968 }
70- return parsed
69+ const normalized = typeof value === 'string' ? value . trim ( ) : value
70+ if ( normalized === '' ) throw new Error ( `${ fieldName } must be a ${ requirement } ` )
71+
72+ let decimal : Decimal
73+ try {
74+ decimal = new Decimal ( normalized )
75+ } catch {
76+ throw new Error ( `${ fieldName } must be a ${ requirement } ` )
77+ }
78+ if ( ! decimal . isFinite ( ) ) throw new Error ( `${ fieldName } must be a ${ requirement } ` )
79+ if ( decimal . decimalPlaces ( ) > 2 ) {
80+ throw new Error ( `${ fieldName } cannot have more than two decimal places` )
81+ }
82+
83+ const number = decimal . toNumber ( )
84+ if (
85+ ! Number . isSafeInteger ( decimal . times ( 100 ) . toNumber ( ) ) ||
86+ ! Number . isFinite ( number ) ||
87+ ! new Decimal ( number ) . equals ( decimal )
88+ ) {
89+ throw new Error ( `${ fieldName } is outside the safely supported amount range` )
90+ }
91+ return decimal
92+ }
93+
94+ function requiredPositiveNumber ( value : unknown , fieldName : string ) : number {
95+ const decimal = quickBooksMoneyDecimal ( value , fieldName , 'positive finite number' )
96+ if ( decimal . lte ( 0 ) ) throw new Error ( `${ fieldName } must be a positive finite number` )
97+ return decimal . toNumber ( )
7198}
7299
73100function optionalPositiveNumber ( value : unknown , fieldName : string ) : number | undefined {
74101 if ( value == null || value === '' ) return undefined
75- const parsed = typeof value === 'number' ? value : Number ( value )
102+ if ( typeof value !== 'number' && typeof value !== 'string' ) {
103+ throw new Error ( `${ fieldName } must be a positive finite number` )
104+ }
105+ const normalized = typeof value === 'string' ? value . trim ( ) : value
106+ if ( normalized === '' ) return undefined
107+ const parsed = typeof normalized === 'number' ? normalized : Number ( normalized )
76108 if ( ! Number . isFinite ( parsed ) || parsed <= 0 ) {
77109 throw new Error ( `${ fieldName } must be a positive finite number` )
78110 }
@@ -86,11 +118,9 @@ function optionalPositiveNumber(value: unknown, fieldName: string): number | und
86118 * a sales form. Only a zero or non-finite value is rejected.
87119 */
88120function requiredNonZeroNumber ( value : unknown , fieldName : string ) : number {
89- const parsed = typeof value === 'number' ? value : Number ( value )
90- if ( ! Number . isFinite ( parsed ) || parsed === 0 ) {
91- throw new Error ( `${ fieldName } must be a non-zero finite number` )
92- }
93- return parsed
121+ const decimal = quickBooksMoneyDecimal ( value , fieldName , 'non-zero finite number' )
122+ if ( decimal . isZero ( ) ) throw new Error ( `${ fieldName } must be a non-zero finite number` )
123+ return decimal . toNumber ( )
94124}
95125
96126function optionalNonZeroNumber ( value : unknown , fieldName : string ) : number | undefined {
@@ -195,12 +225,18 @@ export function parseQuickBooksInvoiceAllocations(
195225 if ( parsed . length > MAX_PAYMENT_ALLOCATIONS ) {
196226 throw new Error ( `${ fieldName } cannot contain more than ${ MAX_PAYMENT_ALLOCATIONS } allocations` )
197227 }
228+ const invoiceIds = new Set < string > ( )
198229 return parsed . map ( ( rawAllocation , index ) => {
199230 const itemName = `${ fieldName } [${ index } ]`
200231 const allocation = assertObject ( rawAllocation , itemName )
201232 assertAllowedKeys ( allocation , PAYMENT_ALLOCATION_KEYS , itemName )
233+ const invoiceId = requiredStringValue ( allocation . invoiceId , `${ itemName } .invoiceId` )
234+ if ( invoiceIds . has ( invoiceId ) ) {
235+ throw new Error ( `${ fieldName } lists invoice ${ invoiceId } more than once` )
236+ }
237+ invoiceIds . add ( invoiceId )
202238 return {
203- invoiceId : requiredStringValue ( allocation . invoiceId , ` ${ itemName } .invoiceId` ) ,
239+ invoiceId,
204240 amount : requiredPositiveNumber ( allocation . amount , `${ itemName } .amount` ) ,
205241 }
206242 } )
0 commit comments