Skip to content

Commit 1990197

Browse files
BillLeoutsakosvl346Bill LeoutsakosBill Leoutsakos
authored
feat(quickbooks): add purchasing and payables (#6159)
* feat(quickbooks): add safe purchasing and payables tools * feat(quickbooks): expose purchasing and payables operations * docs(quickbooks): document purchasing and payables tools * fix(quickbooks): require current purchase payment type * fix(quickbooks): allow rounded purchasing line totals * fix(quickbooks): generate purchasing arrays correctly * fix(quickbooks): validate bill payment accounts * fix(quickbooks): validate bill allocations before account lookup * chore(tools): sync purchasing metadata * fix(quickbooks): sanitize bill payment faults * feat(quickbooks): add general accounting operations (#6185) * feat(quickbooks): add accounting transaction tools * feat(quickbooks): expose accounting operations * docs(quickbooks): generate accounting catalog * fix(quickbooks): preserve accounting amount precision * fix(quickbooks): balance journal entries in exact cents * fix(quickbooks): include account in deposit updates * chore(quickbooks): sync accounting catalog * feat(quickbooks): add observable PO-to-bill linking (#6194) * feat(quickbooks): link bills to purchase order lines * docs(quickbooks): document observable bill linking * fix(quickbooks): document purchase order link identifiers * fix(quickbooks): keep shared line example valid * chore(quickbooks): sync bill linking catalog * feat(quickbooks): add accountant-focused financial reports (#6197) * feat(quickbooks): add verified financial report contracts * feat(quickbooks): expose reports in block and catalog * test(quickbooks): cover null report filters * fix(quickbooks): expose report header time * chore(quickbooks): sync reports catalog * feat(quickbooks): add documents and attachments (#6200) * feat(quickbooks): add document and attachment tools * feat(quickbooks): add bounded document file routes * feat(quickbooks): expose document workflows * fix(quickbooks): enforce attachment upload bounds * fix(quickbooks): tighten document handling * fix(quickbooks): align file response limits * test(quickbooks): cover missing PDF content type * test(quickbooks): cover attachment MIME fallback * fix(quickbooks): redact attachment access URLs * fix(quickbooks): store downloaded documents safely * fix(quickbooks): stop cancelled attachment downloads * fix(quickbooks): correct document schemas and upload bytes * chore(quickbooks): sync document catalog * feat(quickbooks): add accountant filters (#6208) * feat(quickbooks): add safe n8n parity tools * feat(quickbooks): expose accountant parity options * fix(quickbooks): address parity review findings * fix(quickbooks): require recipient for payment email * chore(quickbooks): sync parity catalog --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Mac.localdomain> Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Mac.localdomain> Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Mac.localdomain> Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Bill Leoutsakos <billleoutsakos@Mac.localdomain> --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Mac.localdomain> Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
1 parent 65b5703 commit 1990197

58 files changed

Lines changed: 13823 additions & 674 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/content/docs/en/integrations/quickbooks.mdx

Lines changed: 1488 additions & 55 deletions
Large diffs are not rendered by default.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
3+
import { type NextRequest, NextResponse } from 'next/server'
4+
import { quickBooksAddAttachmentContract } from '@/lib/api/contracts/tools/quickbooks'
5+
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
6+
import { checkInternalAuth } from '@/lib/auth/hybrid'
7+
import { assertKnownSizeWithinLimit, isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
8+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9+
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
10+
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
11+
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
12+
import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
13+
import { assertToolFileAccess } from '@/app/api/files/authorization'
14+
import { buildQuickBooksCompanyUrl, buildQuickBooksHeaders } from '@/tools/quickbooks/client'
15+
import {
16+
assertSingleQuickBooksFile,
17+
buildQuickBooksAttachableMetadata,
18+
getQuickBooksDocumentError,
19+
parseQuickBooksAttachableResponse,
20+
sanitizeQuickBooksFileName,
21+
validateQuickBooksAttachmentFileType,
22+
} from '@/tools/quickbooks/documents_utils'
23+
24+
export const dynamic = 'force-dynamic'
25+
const logger = createLogger('QuickBooksAddAttachmentAPI')
26+
27+
export const POST = withRouteHandler(async (request: NextRequest) => {
28+
const requestId = `quickbooks-attachment-${Date.now()}`
29+
try {
30+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
31+
if (!authResult.success || !authResult.userId) {
32+
return NextResponse.json(
33+
{ success: false, error: authResult.error || 'Unauthorized' },
34+
{ status: 401 }
35+
)
36+
}
37+
38+
const parsed = await parseRequest(
39+
quickBooksAddAttachmentContract,
40+
request,
41+
{},
42+
{
43+
validationErrorResponse: (error) =>
44+
NextResponse.json(
45+
{ success: false, error: getValidationErrorMessage(error, 'Invalid request') },
46+
{ status: 400 }
47+
),
48+
}
49+
)
50+
if (!parsed.success) return parsed.response
51+
const data = parsed.data.body
52+
request.signal.throwIfAborted()
53+
const url = buildQuickBooksCompanyUrl(
54+
data.realmId,
55+
data.attachmentKind === 'file' ? 'upload' : 'attachable'
56+
)
57+
let response: Response
58+
59+
if (data.attachmentKind === 'note') {
60+
const metadata = buildQuickBooksAttachableMetadata(data.targetType, data.targetId, {
61+
note: data.note!,
62+
})
63+
response = await fetch(url, {
64+
method: 'POST',
65+
headers: {
66+
...buildQuickBooksHeaders(data.accessToken),
67+
'Content-Type': 'application/json',
68+
},
69+
body: JSON.stringify(metadata),
70+
signal: request.signal,
71+
})
72+
} else {
73+
request.signal.throwIfAborted()
74+
const rawFile = assertSingleQuickBooksFile(data.file ?? undefined)
75+
const files = processFilesToUserFiles([rawFile], requestId, logger)
76+
if (files.length !== 1) throw new Error('Exactly one valid file is required')
77+
const file = files[0]
78+
assertKnownSizeWithinLimit(file.size, MAX_FILE_SIZE, 'QuickBooks attachment file')
79+
const denied = await assertToolFileAccess(file.key, authResult.userId, requestId, logger)
80+
if (denied) return denied
81+
let downloaded: Awaited<ReturnType<typeof downloadServableFileFromStorage>>
82+
try {
83+
downloaded = await downloadServableFileFromStorage(file, requestId, logger, {
84+
maxBytes: MAX_FILE_SIZE,
85+
signal: request.signal,
86+
})
87+
} catch (error) {
88+
const notReady = docNotReadyResponse(error)
89+
if (notReady) return notReady
90+
throw error
91+
}
92+
request.signal.throwIfAborted()
93+
assertKnownSizeWithinLimit(
94+
downloaded.buffer.length,
95+
MAX_FILE_SIZE,
96+
'QuickBooks attachment file'
97+
)
98+
if (downloaded.buffer.length === 0)
99+
throw new Error('QuickBooks attachment file cannot be empty')
100+
const resolvedName = sanitizeQuickBooksFileName(data.fileName ?? undefined, file.name)
101+
const storedMime = (downloaded.contentType || file.type || '')
102+
.split(';', 1)[0]
103+
.trim()
104+
.toLowerCase()
105+
const requestedMime = data.contentType?.trim().toLowerCase() || storedMime
106+
const mimeType = validateQuickBooksAttachmentFileType(resolvedName, requestedMime)
107+
if (data.contentType && storedMime && requestedMime !== storedMime) {
108+
validateQuickBooksAttachmentFileType(resolvedName, storedMime)
109+
}
110+
const metadata = buildQuickBooksAttachableMetadata(data.targetType, data.targetId, {
111+
fileName: resolvedName,
112+
contentType: mimeType,
113+
description: data.description ?? undefined,
114+
})
115+
const formData = new FormData()
116+
formData.append(
117+
'file_metadata_01',
118+
new Blob([JSON.stringify(metadata)], { type: 'application/json' }),
119+
'attachment.json'
120+
)
121+
formData.append(
122+
'file_content_01',
123+
new Blob(
124+
[
125+
new Uint8Array(
126+
downloaded.buffer.buffer as ArrayBuffer,
127+
downloaded.buffer.byteOffset,
128+
downloaded.buffer.byteLength
129+
),
130+
],
131+
{ type: mimeType }
132+
),
133+
resolvedName
134+
)
135+
request.signal.throwIfAborted()
136+
response = await fetch(url, {
137+
method: 'POST',
138+
headers: buildQuickBooksHeaders(data.accessToken),
139+
body: formData,
140+
signal: request.signal,
141+
})
142+
}
143+
144+
if (!response.ok) throw await getQuickBooksDocumentError(response, request.signal)
145+
const transformed = await parseQuickBooksAttachableResponse(response, request.signal)
146+
return NextResponse.json({
147+
success: true,
148+
output: {
149+
attachment: transformed.attachment,
150+
attachmentId: transformed.attachment.Id.trim(),
151+
attachmentKind: data.attachmentKind,
152+
targetType: data.targetType,
153+
targetId: data.targetId,
154+
time: transformed.time,
155+
},
156+
})
157+
} catch (error) {
158+
logger.error(`[${requestId}] QuickBooks attachment creation failed`, {
159+
error: getErrorMessage(error),
160+
})
161+
return NextResponse.json(
162+
{ success: false, error: getErrorMessage(error, 'Failed to add QuickBooks attachment') },
163+
{ status: isPayloadSizeLimitError(error) ? 413 : 500 }
164+
)
165+
}
166+
})

0 commit comments

Comments
 (0)