Skip to content

Commit 940506a

Browse files
authored
feat(square): add Square integration with 34 commerce operations (#5053)
* feat(square): add Square integration with 34 commerce operations Add a Square integration (API-key auth via personal access token) covering payments, refunds, customers, locations, orders, invoices, catalog, and inventory. Catalog image upload routes through an internal API endpoint using the shared UserFile handling pattern. Adds a dedicated square-errors extractor. * fix(square): correct catalog image part name and address review feedback - Fix catalog image upload: Square's multipart part for the binary is `file`, not `image_file` (per the live API cURL examples); this would have caused upload failures - Catalog image route: check response.ok before parsing, drop the unreachable legacy base64 path, derive MIME from the uploaded file - Block: split the search query field per operation so placeholders match each endpoint's schema; parse each JSON field individually so errors name the field - Round out coverage: complete_payment version_token; customer nickname/birthday; batch inventory states/updated_after/limit * fix(square): correct canonical file param usage and revert query split - Read the catalog image file from the canonical `params.file` (the basic/advanced inputs are collapsed before the params function runs) instead of the raw uploadFile/fileRef ids, which no longer exist at that point — fixes the Canonical Param Validation test and a latent upload bug - Revert the per-operation query split: canonicalParamId is only valid for basic/advanced pairs under one condition. Use a single query field with a schema-neutral placeholder and a wand prompt that covers each search operation * chore(square): trigger fresh review * fix(square): single-location invoice search and guard numeric coercion - SearchInvoices: Square's invoice filter accepts only one location, so take a single locationId (string) instead of an array and wrap it as query.filter.location_ids: [locationId] - Block: fail locally with a clear "<field> must be a valid number" error when amount/limit/version/orderVersion are non-numeric instead of forwarding NaN * fix(square): accept real booleans for autocomplete/includeRelatedObjects Coerce these from both the dropdown's string values and actual booleans (which can arrive via connected blocks or templated inputs), so true is not silently flipped to false. * fix(square): validate parsed JSON field shapes (array vs object) parseJsonField now enforces the expected shape so a valid-but-wrong-type value (e.g. a JSON string where an array is expected for locationIds/objectTypes/ paymentIds/catalogObjectIds/states, or a non-object for order/invoice/etc.) fails locally with a clear message instead of a confusing Square API error.
1 parent 06191a7 commit 940506a

51 files changed

Lines changed: 6821 additions & 2 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/components/icons.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1958,6 +1958,17 @@ export function WhatsAppIcon(props: SVGProps<SVGSVGElement>) {
19581958
)
19591959
}
19601960

1961+
export function SquareIcon(props: SVGProps<SVGSVGElement>) {
1962+
return (
1963+
<svg {...props} viewBox='0 0 1999.98 501.42' xmlns='http://www.w3.org/2000/svg'>
1964+
<path
1965+
fill='#fff'
1966+
d='M501.42,83.79v333.84c0,46.27-37.5,83.79-83.79,83.79H83.79c-46.28,0-83.79-37.5-83.79-83.79V83.79C0,37.51,37.52,0,83.79,0h333.84c46.29,0,83.79,37.5,83.79,83.79h0ZM410.22,117.64c0-14.61-11.85-26.45-26.45-26.45H117.62c-14.61,0-26.45,11.84-26.45,26.45v266.19c0,14.61,11.84,26.45,26.45,26.45h266.17c14.61,0,26.45-11.85,26.45-26.45V117.64h-.02ZM182.31,197.59c0-8.43,6.79-15.26,15.17-15.26h106.4c8.39,0,15.17,6.84,15.17,15.26v106.24c0,8.43-6.75,15.26-15.17,15.26h-106.4c-8.39,0-15.17-6.84-15.17-15.26v-106.24ZM778.93,221.94l-3.85-.86c-41.04-9.31-65.81-14.93-65.81-42,0-24.2,23.02-41.11,55.98-41.11,30.52,0,53.74,12.76,73.08,40.16,1.11,1.57,2.84,2.61,4.74,2.84,1.89.23,3.79-.35,5.23-1.59l32.16-27.71c2.68-2.31,3.15-6.22,1.1-9.09-24.19-33.89-67.01-54.12-114.56-54.12-31.56,0-60.34,9.26-81.04,26.08-21.73,17.65-33.21,41.93-33.21,70.23,0,63.76,54.74,76.94,98.71,87.53,4.45,1.08,8.77,2.1,12.95,3.08,39.74,9.36,66,15.54,66,43.74s-24.04,45.48-61.24,45.48c-33.71,0-64.35-17.1-86.28-48.14-1.1-1.55-2.8-2.59-4.68-2.84s-3.73.28-5.2,1.49l-33.86,27.99c-2.72,2.25-3.28,6.14-1.3,9.05,25.63,37.64,76.48,61.97,129.56,61.97,32.56,0,62.52-9.57,84.36-26.95,23.27-18.51,35.57-44.01,35.57-73.73,0-67.27-57.62-80.13-108.45-91.48l.04-.02ZM1126.33,177.73h-40.76c-3.74,0-6.78,3.04-6.78,6.78v19.06c-12.6-14.21-33.77-30.22-65.18-30.22s-56.88,12.32-75.37,35.62c-17.16,21.63-26.62,51.73-26.62,84.75s9.45,63.12,26.62,84.75c18.49,23.31,44.56,35.62,75.37,35.62,26.63,0,49.1-9.45,65.18-27.37v107.92c0,3.74,3.04,6.78,6.78,6.78h40.76c3.74,0,6.78-3.04,6.78-6.78V184.51c0-3.74-3.04-6.78-6.78-6.78ZM1080.1,287.16v13.57c0,39.86-21.97,65.61-55.98,65.61-36.15,0-57.74-27.15-57.74-72.61s21.58-72.61,57.74-72.61c34.01,0,55.98,25.93,55.98,66.05h0ZM1360.59,177.73h-40.76c-3.74,0-6.78,3.04-6.78,6.78v130.66c0,32.45-23.32,49.42-46.36,49.42-26.03,0-39.79-15.58-39.79-45.04v-135.03c0-3.74-3.04-6.78-6.78-6.78h-40.76c-3.74,0-6.78,3.04-6.78,6.78v146.41c0,50.53,30.93,83.17,78.8,83.17,23.76,0,43.95-9.67,61.67-29.56v17.96c0,3.74,3.04,6.78,6.78,6.78h40.76c3.74,0,6.78-3.04,6.78-6.78v-217.99c0-3.74-3.04-6.78-6.78-6.78h0ZM1607.92,367.06c-8.79-.24-12.72-4.78-12.72-14.7v-98.9c0-51.66-31.71-80.11-89.3-80.11-44.71,0-80.07,23.24-94.59,62.18-.66,1.78-.52,3.78.39,5.47.93,1.73,2.57,2.98,4.48,3.42l37.83,8.71c3.37.77,6.78-1.14,7.94-4.45,6.74-19.11,20.01-28.01,41.76-28.01,25.53,0,38.48,11.62,38.48,34.54v3.2l-62.73,12.98c-53.04,11.03-77.74,34.25-77.74,73.09s32.22,68.73,78.36,68.73c28.25,0,51.2-8.69,66.46-25.16,9.13,17.73,33.93,26.29,62.3,21.35,3.24-.57,5.6-3.38,5.6-6.69v-28.9c0-3.63-2.93-6.67-6.52-6.77v.02ZM1542.2,300.53v26.89c0,27.56-27.28,41.98-54.24,41.98-20.26,0-32.35-10.13-32.35-27.1,0-19.37,14.63-26.41,38.23-31.5l48.36-10.27ZM1774.34,177.32c-3.17-.52-6.47-.77-10.09-.77-27.5,0-50.92,12.35-62.11,32.47v-24.51c0-3.74-3.04-6.78-6.78-6.78h-40.76c-3.74,0-6.78,3.04-6.78,6.78v217.99c0,3.74,3.04,6.78,6.78,6.78h40.76c3.74,0,6.78-3.04,6.78-6.78v-114.9c0-34.27,23.2-57.3,57.74-57.3,4.7,0,8.63.17,12.74.56,1.89.18,3.79-.45,5.2-1.73s2.22-3.11,2.22-5.02v-40.09c0-3.34-2.39-6.16-5.69-6.7h-.01ZM1973.74,206.72c-18.32-21.83-44.82-33.36-76.62-33.36s-59.09,12.34-79.33,34.76c-19.98,22.12-30.98,52.52-30.98,85.61,0,70.87,46.26,120.37,112.49,120.37,43.9,0,79.17-21.53,96.77-59.08.8-1.7.85-3.61.14-5.37-.71-1.76-2.14-3.15-3.91-3.82l-33.69-12.76c-3.39-1.28-7.27.37-8.63,3.68-8.08,19.63-26.56,30.9-50.68,30.9-33.08,0-56.1-23.59-60.26-61.64h154.16c3.74,0,6.78-3.04,6.78-6.78v-11.63c0-31.99-9.32-60.72-26.25-80.88h.01ZM1945.21,264.82h-103.37c7.73-28.91,27.66-45.45,54.84-45.45,34.72,0,47.8,23.3,48.52,45.45h.01Z'
1967+
/>
1968+
</svg>
1969+
)
1970+
}
1971+
19611972
export function StripeIcon(props: SVGProps<SVGSVGElement>) {
19621973
return (
19631974
<svg

apps/docs/components/ui/icon-mapping.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ import {
191191
SlackIcon,
192192
SmtpIcon,
193193
SQSIcon,
194+
SquareIcon,
194195
SshIcon,
195196
STSIcon,
196197
STTIcon,
@@ -439,6 +440,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
439440
slack: SlackIcon,
440441
smtp: SmtpIcon,
441442
sqs: SQSIcon,
443+
square: SquareIcon,
442444
ssh: SshIcon,
443445
stagehand: StagehandIcon,
444446
stripe: StripeIcon,

apps/docs/content/docs/en/integrations/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@
192192
"slack",
193193
"smtp",
194194
"sqs",
195+
"square",
195196
"ssh",
196197
"stagehand",
197198
"stripe",

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

Lines changed: 1479 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
3+
import { generateId } from '@sim/utils/id'
4+
import { type NextRequest, NextResponse } from 'next/server'
5+
import { squareCatalogImageContract } from '@/lib/api/contracts/tools/square'
6+
import { parseRequest } from '@/lib/api/server'
7+
import { checkInternalAuth } from '@/lib/auth/hybrid'
8+
import { generateRequestId } from '@/lib/core/utils/request'
9+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
10+
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
11+
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
12+
import { assertToolFileAccess } from '@/app/api/files/authorization'
13+
import { SQUARE_API_VERSION, SQUARE_BASE_URL } from '@/tools/square/types'
14+
15+
export const dynamic = 'force-dynamic'
16+
17+
const logger = createLogger('SquareCatalogImageAPI')
18+
19+
export const POST = withRouteHandler(async (request: NextRequest) => {
20+
const requestId = generateRequestId()
21+
22+
try {
23+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
24+
25+
if (!authResult.success || !authResult.userId) {
26+
logger.warn(`[${requestId}] Unauthorized Square catalog image upload: ${authResult.error}`)
27+
return NextResponse.json(
28+
{ success: false, error: authResult.error || 'Authentication required' },
29+
{ status: 401 }
30+
)
31+
}
32+
33+
const parsed = await parseRequest(squareCatalogImageContract, request, {})
34+
if (!parsed.success) return parsed.response
35+
const validatedData = parsed.data.body
36+
37+
if (!validatedData.file) {
38+
return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 })
39+
}
40+
41+
const userFiles = processFilesToUserFiles(
42+
[validatedData.file as RawFileInput],
43+
requestId,
44+
logger
45+
)
46+
47+
if (userFiles.length === 0) {
48+
return NextResponse.json({ success: false, error: 'Invalid file input' }, { status: 400 })
49+
}
50+
51+
const userFile = userFiles[0]
52+
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
53+
if (denied) return denied
54+
55+
const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
56+
const fileName = validatedData.fileName || userFile.name
57+
const mimeType = userFile.type || 'application/octet-stream'
58+
59+
const imageRequest: Record<string, unknown> = {
60+
idempotency_key: validatedData.idempotencyKey || generateId(),
61+
image: {
62+
type: 'IMAGE',
63+
id: '#square_catalog_image',
64+
image_data: validatedData.caption ? { caption: validatedData.caption } : {},
65+
},
66+
}
67+
if (validatedData.objectId) imageRequest.object_id = validatedData.objectId
68+
69+
const formData = new FormData()
70+
formData.append('request', JSON.stringify(imageRequest))
71+
formData.append('file', new Blob([new Uint8Array(fileBuffer)], { type: mimeType }), fileName)
72+
73+
const response = await fetch(`${SQUARE_BASE_URL}/v2/catalog/images`, {
74+
method: 'POST',
75+
headers: {
76+
Authorization: `Bearer ${validatedData.accessToken}`,
77+
'Square-Version': SQUARE_API_VERSION,
78+
},
79+
body: formData,
80+
})
81+
82+
if (!response.ok) {
83+
const errorText = await response.text()
84+
let detail: string | undefined
85+
try {
86+
detail = JSON.parse(errorText)?.errors?.[0]?.detail
87+
} catch {
88+
detail = undefined
89+
}
90+
logger.error(`[${requestId}] Square API error:`, { status: response.status, body: errorText })
91+
return NextResponse.json(
92+
{
93+
success: false,
94+
error: detail || `Failed to upload catalog image (HTTP ${response.status})`,
95+
},
96+
{ status: response.status }
97+
)
98+
}
99+
100+
const data = await response.json()
101+
const object = data.image ?? {}
102+
103+
return NextResponse.json({
104+
success: true,
105+
output: {
106+
object,
107+
metadata: {
108+
id: object.id ?? '',
109+
type: object.type ?? null,
110+
version: object.version ?? null,
111+
},
112+
},
113+
})
114+
} catch (error) {
115+
logger.error(`[${requestId}] Unexpected error:`, error)
116+
return NextResponse.json(
117+
{ success: false, error: getErrorMessage(error, 'Unknown error') },
118+
{ status: 500 }
119+
)
120+
}
121+
})

0 commit comments

Comments
 (0)