@@ -21,6 +21,17 @@ import { quickbooksReadMasterDataTool } from '@/tools/quickbooks/read_master_dat
2121import { quickbooksReadPurchasingTransactionsTool } from '@/tools/quickbooks/read_purchasing_transactions'
2222import { quickbooksReadSalesTransactionsTool } from '@/tools/quickbooks/read_sales_transactions'
2323import type { QuickBooksAttachmentTargetType } from '@/tools/quickbooks/types'
24+ import {
25+ buildQuickBooksMasterDataQueryUrl ,
26+ buildQuickBooksQueryUrl ,
27+ buildQuickBooksSalesQueryUrl ,
28+ } from '@/tools/quickbooks/utils'
29+ import {
30+ parseQuickBooksAddress ,
31+ quickBooksDisplayName ,
32+ quickBooksEmailAddress ,
33+ quickBooksItemName ,
34+ } from '@/tools/quickbooks/values'
2435
2536describe ( 'QuickBooks credential authority' , ( ) => {
2637 it ( 'declares both credential-derived routing parameters on every tool' , ( ) => {
@@ -332,3 +343,115 @@ describe('QuickBooks locale-dependent item accounts', () => {
332343 ) . toEqual ( { Name : 'Conseil' , Type : 'Service' } )
333344 } )
334345} )
346+
347+ describe ( 'QuickBooks documented query language' , ( ) => {
348+ const auth = {
349+ accessToken : 'token' ,
350+ realmId : '123' ,
351+ quickBooksEnvironment : 'sandbox' ,
352+ } as const
353+
354+ function getQueryStatement ( url : URL ) : string {
355+ return url . searchParams . get ( 'query' ) ?? ''
356+ }
357+
358+ it ( 'percent-encodes spaces rather than using the form-encoded plus' , ( ) => {
359+ const url = buildQuickBooksQueryUrl ( auth , 'Customer' , 1 , 25 )
360+
361+ expect ( url . search ) . not . toContain ( '+' )
362+ expect ( url . search ) . toContain (
363+ 'query=SELECT%20*%20FROM%20Customer%20STARTPOSITION%201%20MAXRESULTS%2025'
364+ )
365+ } )
366+
367+ it ( 'emits one-based STARTPOSITION and MAXRESULTS in the documented order' , ( ) => {
368+ expect ( getQueryStatement ( buildQuickBooksQueryUrl ( auth , 'Invoice' , 26 , 25 ) ) ) . toBe (
369+ 'SELECT * FROM Invoice STARTPOSITION 26 MAXRESULTS 25'
370+ )
371+ } )
372+
373+ it ( 'renders a boolean Active filter unquoted' , ( ) => {
374+ expect (
375+ getQueryStatement (
376+ buildQuickBooksMasterDataQueryUrl ( {
377+ ...auth ,
378+ recordType : 'vendor' ,
379+ readMode : 'list' ,
380+ activeStatus : 'inactive' ,
381+ } )
382+ )
383+ ) . toBe ( 'SELECT * FROM Vendor WHERE Active = false STARTPOSITION 1 MAXRESULTS 25' )
384+ } )
385+
386+ it ( 'omits the WHERE clause when no filter is requested' , ( ) => {
387+ expect (
388+ getQueryStatement (
389+ buildQuickBooksMasterDataQueryUrl ( { ...auth , recordType : 'item' , readMode : 'list' } )
390+ )
391+ ) . toBe ( 'SELECT * FROM Item STARTPOSITION 1 MAXRESULTS 25' )
392+ } )
393+
394+ it ( 'escapes backslashes before apostrophes in a filter value' , ( ) => {
395+ expect (
396+ getQueryStatement (
397+ buildQuickBooksSalesQueryUrl ( {
398+ ...auth ,
399+ transactionType : 'invoice' ,
400+ readMode : 'list' ,
401+ customerId : "a\\b'c" ,
402+ } )
403+ )
404+ ) . toBe ( "SELECT * FROM Invoice WHERE CustomerRef = 'a\\\\b\\'c' STARTPOSITION 1 MAXRESULTS 25" )
405+ } )
406+
407+ it ( 'accepts the documented maximum page size of 1000' , ( ) => {
408+ expect ( getQueryStatement ( buildQuickBooksQueryUrl ( auth , 'Customer' , 1 , 1000 ) ) ) . toBe (
409+ 'SELECT * FROM Customer STARTPOSITION 1 MAXRESULTS 1000'
410+ )
411+ expect ( ( ) => buildQuickBooksQueryUrl ( auth , 'Customer' , 1 , 1001 ) ) . toThrow (
412+ 'maxResults must be an integer from 1 through 1000'
413+ )
414+ } )
415+ } )
416+
417+ describe ( 'QuickBooks documented field constraints' , ( ) => {
418+ it ( 'writes back every address line QuickBooks returns' , ( ) => {
419+ expect (
420+ parseQuickBooksAddress (
421+ { Line1 : '1' , Line2 : '2' , Line3 : '3' , Line4 : '4' , Line5 : '5' } ,
422+ 'billingAddress'
423+ )
424+ ) . toEqual ( { Line1 : '1' , Line2 : '2' , Line3 : '3' , Line4 : '4' , Line5 : '5' } )
425+ expect ( parseQuickBooksAddress ( { line3 : '3' } , 'billingAddress' ) ) . toEqual ( { Line3 : '3' } )
426+ } )
427+
428+ it ( 'rejects an item name QuickBooks documents as invalid' , ( ) => {
429+ expect ( ( ) => quickBooksItemName ( 'Parent:Child' ) ) . toThrow (
430+ 'name cannot include tabs, new lines, or colons'
431+ )
432+ expect ( ( ) => quickBooksItemName ( 'a' . repeat ( 101 ) ) ) . toThrow ( 'name cannot exceed 100 characters' )
433+ expect ( quickBooksItemName ( 'Consulting' ) ) . toBe ( 'Consulting' )
434+ } )
435+
436+ it ( 'rejects a display name past the documented 500-character maximum' , ( ) => {
437+ expect ( ( ) => quickBooksDisplayName ( 'a' . repeat ( 501 ) , 'displayName' ) ) . toThrow (
438+ 'displayName cannot exceed 500 characters'
439+ )
440+ expect ( quickBooksDisplayName ( 'a' . repeat ( 500 ) , 'displayName' ) ) . toHaveLength ( 500 )
441+ } )
442+
443+ it ( 'rejects an email address QuickBooks cannot store' , ( ) => {
444+ expect ( ( ) => quickBooksEmailAddress ( 'not-an-email' ) ) . toThrow (
445+ 'Email address must be a valid address such as name@example.com'
446+ )
447+ expect ( ( ) => quickBooksEmailAddress ( 'a@b@c.com' ) ) . toThrow (
448+ 'Email address must be a valid address such as name@example.com'
449+ )
450+ expect ( ( ) => quickBooksEmailAddress ( `${ 'a' . repeat ( 95 ) } @example.com` ) ) . toThrow (
451+ 'Email address cannot exceed 100 characters'
452+ )
453+ expect ( quickBooksEmailAddress ( 'billing@example.com' ) ) . toEqual ( {
454+ Address : 'billing@example.com' ,
455+ } )
456+ } )
457+ } )
0 commit comments