Skip to content

Commit dbec57c

Browse files
committed
Merge branch 'fix/qb-internal-boundary' into integration/quickbooks-doc-alignment
2 parents d51a1d6 + dab93a3 commit dbec57c

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)