Skip to content

Commit dab93a3

Browse files
waleedlatif1claude
andcommitted
fix(quickbooks): gate every internal tool operation and contract-bind the provider ops
The QuickBooks internal handler returned for all twelve provider create/update tool ids above both the operation input cap and the trusted-identity check, so those gates only ever ran for the three file tool ids. Hoist both above the switch, matching the Asana handler. The same twelve operations had no boundary schema — executeToolOperationImplementation only checks the input is a non-array object before casting to the operation's param type. Author a contract per operation and route them through executeInternalJsonToolOperation, the canonical in-process path. The two file operations now parse through their contracts as well, which were previously declared but unreferenced. Also pass the transfer signal, not the caller's, when reading a failed transaction-PDF response body, so a stalled Intuit error body stays bounded by the 60s transfer deadline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB
1 parent 07f9190 commit dab93a3

5 files changed

Lines changed: 650 additions & 86 deletions

File tree

‎apps/sim/lib/api/contracts/tools/quickbooks.ts‎

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod'
22
import { userFileSchema } from '@/lib/api/contracts/primitives'
3+
import type { ContractBody, ContractJsonResponse } from '@/lib/api/contracts/types'
34
import { defineRouteContract } from '@/lib/api/contracts/types'
45
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
56

@@ -198,3 +199,352 @@ export const quickBooksAddAttachmentContract = defineRouteContract({
198199
]),
199200
},
200201
})
202+
203+
const QUICKBOOKS_MAX_LINES = 100
204+
const QUICKBOOKS_MAX_ALLOCATIONS = 100
205+
206+
function requiredQuickBooksId(label: string) {
207+
return z.string().min(1, `${label} is required`).max(256, `${label} is too long`)
208+
}
209+
210+
function optionalQuickBooksId(label: string) {
211+
return z.string().max(256, `${label} is too long`).optional()
212+
}
213+
214+
function optionalQuickBooksText(label: string, max: number) {
215+
return z.string().max(max, `${label} is too long`).optional()
216+
}
217+
218+
/**
219+
* QuickBooks dates are `YYYY-MM-DD`, but the format check stays in
220+
* `validateQuickBooksDate` so an empty value keeps meaning "not supplied".
221+
*/
222+
function optionalQuickBooksDate(label: string) {
223+
return z.string().max(32, `${label} is too long`).optional()
224+
}
225+
226+
const quickBooksActiveStatusSchema = z
227+
.enum(['unchanged', 'active', 'inactive'], {
228+
error: 'activeStatus must be unchanged, active, or inactive',
229+
})
230+
.optional()
231+
232+
/**
233+
* Address input reaches this boundary already parsed into an object on every
234+
* caller path. Key names and their QuickBooks mapping stay in
235+
* `parseQuickBooksAddress`.
236+
*/
237+
const quickBooksAddressInputSchema = z.record(z.string(), z.string())
238+
239+
const quickBooksSalesLineInputSchema = z.strictObject({
240+
lineType: z.enum(['item', 'description'], {
241+
error: 'lines[].lineType must be item or description',
242+
}),
243+
amount: z.number().optional(),
244+
itemId: optionalQuickBooksId('lines[].itemId'),
245+
description: optionalQuickBooksText('lines[].description', 4000),
246+
quantity: z.number().optional(),
247+
unitPrice: z.number().optional(),
248+
serviceDate: optionalQuickBooksDate('lines[].serviceDate'),
249+
})
250+
251+
const quickBooksSalesLinesSchema = z
252+
.array(quickBooksSalesLineInputSchema)
253+
.min(1, 'lines must contain at least one line')
254+
.max(QUICKBOOKS_MAX_LINES, `lines cannot contain more than ${QUICKBOOKS_MAX_LINES} lines`)
255+
256+
const quickBooksInvoiceAllocationsSchema = z
257+
.array(
258+
z.strictObject({
259+
invoiceId: requiredQuickBooksId('invoiceAllocations[].invoiceId'),
260+
amount: z.number(),
261+
})
262+
)
263+
.min(1, 'invoiceAllocations must contain at least one allocation')
264+
.max(
265+
QUICKBOOKS_MAX_ALLOCATIONS,
266+
`invoiceAllocations cannot contain more than ${QUICKBOOKS_MAX_ALLOCATIONS} allocations`
267+
)
268+
269+
const quickBooksBillAllocationsSchema = z
270+
.array(
271+
z.strictObject({
272+
billId: requiredQuickBooksId('billAllocations[].billId'),
273+
amount: z.number(),
274+
})
275+
)
276+
.min(1, 'billAllocations must contain at least one allocation')
277+
.max(
278+
QUICKBOOKS_MAX_ALLOCATIONS,
279+
`billAllocations cannot contain more than ${QUICKBOOKS_MAX_ALLOCATIONS} allocations`
280+
)
281+
282+
/** Every QuickBooks create/update operation answers with the same mutation envelope. */
283+
const quickBooksMutationResponseSchema = z.object({
284+
success: z.literal(true),
285+
output: z.object({
286+
record: z
287+
.object({
288+
Id: z.string().min(1),
289+
SyncToken: z.string().optional(),
290+
})
291+
.passthrough(),
292+
recordId: boundedId,
293+
syncToken: z.string().min(1),
294+
recordVersion: z.string().min(1),
295+
time: z.string().nullable(),
296+
}),
297+
})
298+
299+
const quickBooksMutationResponse = {
300+
mode: 'json',
301+
schema: quickBooksMutationResponseSchema,
302+
} as const
303+
304+
export const quickBooksCreateBillPaymentBodySchema = quickBooksAuthSchema.extend({
305+
vendorId: requiredQuickBooksId('vendorId'),
306+
totalAmount: z.number(),
307+
paymentType: z.enum(['check', 'credit_card'], {
308+
error: 'paymentType must be check or credit_card',
309+
}),
310+
paymentAccountId: requiredQuickBooksId('paymentAccountId'),
311+
billAllocations: quickBooksBillAllocationsSchema.optional(),
312+
transactionDate: optionalQuickBooksDate('transactionDate'),
313+
privateNote: optionalQuickBooksText('privateNote', 4000),
314+
requestId: optionalQuickBooksText('requestId', 256),
315+
})
316+
317+
export const quickBooksUpdateBillBodySchema = quickBooksAuthSchema.extend({
318+
billId: requiredQuickBooksId('billId'),
319+
syncToken: requiredQuickBooksId('syncToken'),
320+
vendorId: optionalQuickBooksId('vendorId'),
321+
apAccountId: optionalQuickBooksId('apAccountId'),
322+
transactionDate: optionalQuickBooksDate('transactionDate'),
323+
dueDate: optionalQuickBooksDate('dueDate'),
324+
documentNumber: optionalQuickBooksText('documentNumber', 256),
325+
privateNote: optionalQuickBooksText('privateNote', 4000),
326+
})
327+
328+
export const quickBooksUpdateBillPaymentBodySchema = quickBooksAuthSchema.extend({
329+
billPaymentId: requiredQuickBooksId('billPaymentId'),
330+
syncToken: requiredQuickBooksId('syncToken'),
331+
vendorId: optionalQuickBooksId('vendorId'),
332+
transactionDate: optionalQuickBooksDate('transactionDate'),
333+
privateNote: optionalQuickBooksText('privateNote', 4000),
334+
})
335+
336+
/** Credit memos and refund receipts share Intuit's sales-document update shape. */
337+
export const quickBooksUpdateSalesDocumentBodySchema = quickBooksAuthSchema.extend({
338+
transactionId: requiredQuickBooksId('transactionId'),
339+
syncToken: requiredQuickBooksId('syncToken'),
340+
customerId: optionalQuickBooksId('customerId'),
341+
lines: quickBooksSalesLinesSchema.optional(),
342+
transactionDate: optionalQuickBooksDate('transactionDate'),
343+
documentNumber: optionalQuickBooksText('documentNumber', 256),
344+
privateNote: optionalQuickBooksText('privateNote', 4000),
345+
customerMemo: optionalQuickBooksText('customerMemo', 4000),
346+
dueDate: optionalQuickBooksDate('dueDate'),
347+
expirationDate: optionalQuickBooksDate('expirationDate'),
348+
paymentMethodId: optionalQuickBooksId('paymentMethodId'),
349+
paymentReferenceNumber: optionalQuickBooksText('paymentReferenceNumber', 256),
350+
depositAccountId: optionalQuickBooksId('depositAccountId'),
351+
})
352+
353+
export const quickBooksUpdateCustomerPaymentBodySchema = quickBooksAuthSchema.extend({
354+
paymentId: requiredQuickBooksId('paymentId'),
355+
syncToken: requiredQuickBooksId('syncToken'),
356+
customerId: optionalQuickBooksId('customerId'),
357+
totalAmount: z.number().optional(),
358+
transactionDate: optionalQuickBooksDate('transactionDate'),
359+
privateNote: optionalQuickBooksText('privateNote', 4000),
360+
paymentReferenceNumber: optionalQuickBooksText('paymentReferenceNumber', 256),
361+
paymentMethodId: optionalQuickBooksId('paymentMethodId'),
362+
depositAccountId: optionalQuickBooksId('depositAccountId'),
363+
invoiceAllocations: quickBooksInvoiceAllocationsSchema.optional(),
364+
unapplyOmittedInvoices: z.boolean().optional(),
365+
})
366+
367+
export const quickBooksUpdateEmployeeBodySchema = quickBooksAuthSchema.extend({
368+
employeeId: requiredQuickBooksId('employeeId'),
369+
syncToken: requiredQuickBooksId('syncToken'),
370+
displayName: optionalQuickBooksText('displayName', 1000),
371+
givenName: optionalQuickBooksText('givenName', 1000),
372+
familyName: optionalQuickBooksText('familyName', 1000),
373+
primaryEmail: optionalQuickBooksText('primaryEmail', 320),
374+
primaryPhone: optionalQuickBooksText('primaryPhone', 100),
375+
primaryAddress: quickBooksAddressInputSchema.optional(),
376+
printOnCheckName: optionalQuickBooksText('printOnCheckName', 1000),
377+
billableTime: z.boolean().optional(),
378+
activeStatus: quickBooksActiveStatusSchema,
379+
})
380+
381+
export const quickBooksUpdateItemBodySchema = quickBooksAuthSchema.extend({
382+
itemId: requiredQuickBooksId('itemId'),
383+
syncToken: requiredQuickBooksId('syncToken'),
384+
name: optionalQuickBooksText('name', 1000),
385+
incomeAccountId: optionalQuickBooksId('incomeAccountId'),
386+
description: optionalQuickBooksText('description', 4000),
387+
unitPrice: z.number().optional(),
388+
purchaseDescription: optionalQuickBooksText('purchaseDescription', 4000),
389+
purchaseCost: z.number().optional(),
390+
expenseAccountId: optionalQuickBooksId('expenseAccountId'),
391+
taxable: z.boolean().optional(),
392+
activeStatus: quickBooksActiveStatusSchema,
393+
})
394+
395+
export const quickBooksUpdatePurchaseBodySchema = quickBooksAuthSchema.extend({
396+
purchaseId: requiredQuickBooksId('purchaseId'),
397+
syncToken: requiredQuickBooksId('syncToken'),
398+
vendorId: optionalQuickBooksId('vendorId'),
399+
transactionDate: optionalQuickBooksDate('transactionDate'),
400+
paymentReference: optionalQuickBooksText('paymentReference', 256),
401+
privateNote: optionalQuickBooksText('privateNote', 4000),
402+
})
403+
404+
export const quickBooksUpdatePurchaseOrderBodySchema = quickBooksAuthSchema.extend({
405+
purchaseOrderId: requiredQuickBooksId('purchaseOrderId'),
406+
syncToken: requiredQuickBooksId('syncToken'),
407+
vendorId: optionalQuickBooksId('vendorId'),
408+
apAccountId: optionalQuickBooksId('apAccountId'),
409+
transactionDate: optionalQuickBooksDate('transactionDate'),
410+
documentNumber: optionalQuickBooksText('documentNumber', 256),
411+
privateNote: optionalQuickBooksText('privateNote', 4000),
412+
})
413+
414+
export const quickBooksUpdateVendorBodySchema = quickBooksAuthSchema.extend({
415+
vendorId: requiredQuickBooksId('vendorId'),
416+
syncToken: requiredQuickBooksId('syncToken'),
417+
displayName: optionalQuickBooksText('displayName', 1000),
418+
companyName: optionalQuickBooksText('companyName', 1000),
419+
givenName: optionalQuickBooksText('givenName', 1000),
420+
familyName: optionalQuickBooksText('familyName', 1000),
421+
primaryEmail: optionalQuickBooksText('primaryEmail', 320),
422+
primaryPhone: optionalQuickBooksText('primaryPhone', 100),
423+
billingAddress: quickBooksAddressInputSchema.optional(),
424+
printOnCheckName: optionalQuickBooksText('printOnCheckName', 1000),
425+
accountNumber: optionalQuickBooksText('accountNumber', 256),
426+
vendor1099: z.boolean().optional(),
427+
activeStatus: quickBooksActiveStatusSchema,
428+
})
429+
430+
export const quickBooksUpdateVendorCreditBodySchema = quickBooksAuthSchema.extend({
431+
vendorCreditId: requiredQuickBooksId('vendorCreditId'),
432+
syncToken: requiredQuickBooksId('syncToken'),
433+
vendorId: optionalQuickBooksId('vendorId'),
434+
apAccountId: optionalQuickBooksId('apAccountId'),
435+
transactionDate: optionalQuickBooksDate('transactionDate'),
436+
documentNumber: optionalQuickBooksText('documentNumber', 256),
437+
privateNote: optionalQuickBooksText('privateNote', 4000),
438+
})
439+
440+
export const quickBooksCreateBillPaymentContract = defineRouteContract({
441+
method: 'POST',
442+
path: '/api/tools/quickbooks/create-bill-payment',
443+
body: quickBooksCreateBillPaymentBodySchema,
444+
response: quickBooksMutationResponse,
445+
})
446+
447+
export const quickBooksUpdateBillContract = defineRouteContract({
448+
method: 'POST',
449+
path: '/api/tools/quickbooks/update-bill',
450+
body: quickBooksUpdateBillBodySchema,
451+
response: quickBooksMutationResponse,
452+
})
453+
454+
export const quickBooksUpdateBillPaymentContract = defineRouteContract({
455+
method: 'POST',
456+
path: '/api/tools/quickbooks/update-bill-payment',
457+
body: quickBooksUpdateBillPaymentBodySchema,
458+
response: quickBooksMutationResponse,
459+
})
460+
461+
export const quickBooksUpdateCreditMemoContract = defineRouteContract({
462+
method: 'POST',
463+
path: '/api/tools/quickbooks/update-credit-memo',
464+
body: quickBooksUpdateSalesDocumentBodySchema,
465+
response: quickBooksMutationResponse,
466+
})
467+
468+
export const quickBooksUpdateCustomerPaymentContract = defineRouteContract({
469+
method: 'POST',
470+
path: '/api/tools/quickbooks/update-customer-payment',
471+
body: quickBooksUpdateCustomerPaymentBodySchema,
472+
response: quickBooksMutationResponse,
473+
})
474+
475+
export const quickBooksUpdateEmployeeContract = defineRouteContract({
476+
method: 'POST',
477+
path: '/api/tools/quickbooks/update-employee',
478+
body: quickBooksUpdateEmployeeBodySchema,
479+
response: quickBooksMutationResponse,
480+
})
481+
482+
export const quickBooksUpdateItemContract = defineRouteContract({
483+
method: 'POST',
484+
path: '/api/tools/quickbooks/update-item',
485+
body: quickBooksUpdateItemBodySchema,
486+
response: quickBooksMutationResponse,
487+
})
488+
489+
export const quickBooksUpdatePurchaseContract = defineRouteContract({
490+
method: 'POST',
491+
path: '/api/tools/quickbooks/update-purchase',
492+
body: quickBooksUpdatePurchaseBodySchema,
493+
response: quickBooksMutationResponse,
494+
})
495+
496+
export const quickBooksUpdatePurchaseOrderContract = defineRouteContract({
497+
method: 'POST',
498+
path: '/api/tools/quickbooks/update-purchase-order',
499+
body: quickBooksUpdatePurchaseOrderBodySchema,
500+
response: quickBooksMutationResponse,
501+
})
502+
503+
export const quickBooksUpdateRefundReceiptContract = defineRouteContract({
504+
method: 'POST',
505+
path: '/api/tools/quickbooks/update-refund-receipt',
506+
body: quickBooksUpdateSalesDocumentBodySchema,
507+
response: quickBooksMutationResponse,
508+
})
509+
510+
export const quickBooksUpdateVendorContract = defineRouteContract({
511+
method: 'POST',
512+
path: '/api/tools/quickbooks/update-vendor',
513+
body: quickBooksUpdateVendorBodySchema,
514+
response: quickBooksMutationResponse,
515+
})
516+
517+
export const quickBooksUpdateVendorCreditContract = defineRouteContract({
518+
method: 'POST',
519+
path: '/api/tools/quickbooks/update-vendor-credit',
520+
body: quickBooksUpdateVendorCreditBodySchema,
521+
response: quickBooksMutationResponse,
522+
})
523+
524+
export type QuickBooksCreateBillPaymentBody = ContractBody<
525+
typeof quickBooksCreateBillPaymentContract
526+
>
527+
export type QuickBooksUpdateBillBody = ContractBody<typeof quickBooksUpdateBillContract>
528+
export type QuickBooksUpdateBillPaymentBody = ContractBody<
529+
typeof quickBooksUpdateBillPaymentContract
530+
>
531+
export type QuickBooksUpdateCreditMemoBody = ContractBody<typeof quickBooksUpdateCreditMemoContract>
532+
export type QuickBooksUpdateCustomerPaymentBody = ContractBody<
533+
typeof quickBooksUpdateCustomerPaymentContract
534+
>
535+
export type QuickBooksUpdateEmployeeBody = ContractBody<typeof quickBooksUpdateEmployeeContract>
536+
export type QuickBooksUpdateItemBody = ContractBody<typeof quickBooksUpdateItemContract>
537+
export type QuickBooksUpdatePurchaseBody = ContractBody<typeof quickBooksUpdatePurchaseContract>
538+
export type QuickBooksUpdatePurchaseOrderBody = ContractBody<
539+
typeof quickBooksUpdatePurchaseOrderContract
540+
>
541+
export type QuickBooksUpdateRefundReceiptBody = ContractBody<
542+
typeof quickBooksUpdateRefundReceiptContract
543+
>
544+
export type QuickBooksUpdateVendorBody = ContractBody<typeof quickBooksUpdateVendorContract>
545+
export type QuickBooksUpdateVendorCreditBody = ContractBody<
546+
typeof quickBooksUpdateVendorCreditContract
547+
>
548+
export type QuickBooksMutationOperationResponse = ContractJsonResponse<
549+
typeof quickBooksUpdateVendorContract
550+
>

0 commit comments

Comments
 (0)