@wesell/n8n-nodes-confirmx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/credentials/ConfirmXApi.credentials.d.ts +18 -0
- package/credentials/ConfirmXApi.credentials.js +31 -0
- package/credentials/ConfirmXApi.credentials.js.map +1 -0
- package/credentials/ConfirmXApi.credentials.ts +40 -0
- package/index.d.ts +14 -0
- package/index.js +26 -0
- package/nodes/ConfirmX/ConfirmXAccount.node.d.ts +5 -0
- package/nodes/ConfirmX/ConfirmXAccount.node.js +81 -0
- package/nodes/ConfirmX/ConfirmXAccount.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXAccount.node.ts +81 -0
- package/nodes/ConfirmX/ConfirmXConversation.node.d.ts +13 -0
- package/nodes/ConfirmX/ConfirmXConversation.node.js +266 -0
- package/nodes/ConfirmX/ConfirmXConversation.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXConversation.node.ts +263 -0
- package/nodes/ConfirmX/ConfirmXMessage.node.d.ts +13 -0
- package/nodes/ConfirmX/ConfirmXMessage.node.js +364 -0
- package/nodes/ConfirmX/ConfirmXMessage.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXMessage.node.ts +361 -0
- package/nodes/ConfirmX/ConfirmXShippingZone.node.d.ts +5 -0
- package/nodes/ConfirmX/ConfirmXShippingZone.node.js +100 -0
- package/nodes/ConfirmX/ConfirmXShippingZone.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXShippingZone.node.ts +103 -0
- package/nodes/ConfirmX/ConfirmXTemplate.node.d.ts +13 -0
- package/nodes/ConfirmX/ConfirmXTemplate.node.js +310 -0
- package/nodes/ConfirmX/ConfirmXTemplate.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXTemplate.node.ts +310 -0
- package/nodes/ConfirmX/ConfirmXTrigger.node.d.ts +29 -0
- package/nodes/ConfirmX/ConfirmXTrigger.node.js +190 -0
- package/nodes/ConfirmX/ConfirmXTrigger.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXTrigger.node.ts +245 -0
- package/nodes/ConfirmX/ConfirmXWebhook.node.d.ts +5 -0
- package/nodes/ConfirmX/ConfirmXWebhook.node.js +169 -0
- package/nodes/ConfirmX/ConfirmXWebhook.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXWebhook.node.ts +163 -0
- package/nodes/ConfirmX/confirmx.svg +4 -0
- package/package.json +69 -0
- package/transports/http.d.ts +43 -0
- package/transports/http.js +117 -0
- package/transports/http.js.map +1 -0
- package/transports/http.ts +170 -0
- package/transports/signature.d.ts +21 -0
- package/transports/signature.js +50 -0
- package/transports/signature.js.map +1 -0
- package/transports/signature.ts +55 -0
- package/types/api.d.ts +199 -0
- package/types/api.js +21 -0
- package/types/api.js.map +1 -0
- package/types/api.ts +238 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow'
|
|
2
|
+
import { confirmxApiRequest, loadAccountOptions } from '../../transports/http'
|
|
3
|
+
|
|
4
|
+
export class ConfirmXMessage implements INodeType {
|
|
5
|
+
description: INodeTypeDescription = {
|
|
6
|
+
displayName: 'ConfirmX Message',
|
|
7
|
+
name: 'confirmXMessage',
|
|
8
|
+
icon: 'file:confirmx.svg',
|
|
9
|
+
group: ['transform'],
|
|
10
|
+
version: 1,
|
|
11
|
+
subtitle: '={{$parameter["operation"]}}',
|
|
12
|
+
description:
|
|
13
|
+
'Send WhatsApp messages, list messages in a conversation, start a new conversation with a template, or retry a failed message.',
|
|
14
|
+
defaults: { name: 'ConfirmX Message' },
|
|
15
|
+
inputs: ['main'],
|
|
16
|
+
outputs: ['main'],
|
|
17
|
+
credentials: [{ name: 'confirmXApi', required: true }],
|
|
18
|
+
properties: [
|
|
19
|
+
{
|
|
20
|
+
displayName: 'Operation',
|
|
21
|
+
name: 'operation',
|
|
22
|
+
type: 'options',
|
|
23
|
+
noDataExpression: true,
|
|
24
|
+
options: [
|
|
25
|
+
{
|
|
26
|
+
name: 'Send Text',
|
|
27
|
+
value: 'sendText',
|
|
28
|
+
action: 'Send text message',
|
|
29
|
+
description:
|
|
30
|
+
'Send a free-form text message in an existing conversation. Use ONLY when the conversation is open (within 24h of the customer\'s last inbound message); otherwise use `startConversation` with a template. Requires `conversationId` and `body`. Optional `replyToMessageId` to quote a previous message.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'Send Media',
|
|
34
|
+
value: 'sendMedia',
|
|
35
|
+
action: 'Send media message',
|
|
36
|
+
description:
|
|
37
|
+
'Send a media message (image, video, document, audio) inside an existing conversation. Requires `conversationId` and `mediaUrl` (publicly fetchable URL); for documents/audio/voice also supply `mediaFilename`. Use ONLY within the 24h messaging window.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Start Conversation',
|
|
41
|
+
value: 'startConversation',
|
|
42
|
+
action: 'Start conversation with template',
|
|
43
|
+
description:
|
|
44
|
+
"Open a new WhatsApp conversation with a customer by sending an approved template. This bypasses the 24h messaging window — it's how you reach a customer proactively. Requires `accountId`, `phoneNumber` (E.164), and `templateName`. Returns the new conversation id you can use for follow-up messages.",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'List Messages',
|
|
48
|
+
value: 'listMessages',
|
|
49
|
+
action: 'List messages in conversation',
|
|
50
|
+
description:
|
|
51
|
+
'Page through the messages in a conversation. Returns up to `limit` messages per page with an opaque `nextCursor` for the next page. Requires `conversationId`.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'Retry',
|
|
55
|
+
value: 'retry',
|
|
56
|
+
action: 'Retry a failed message',
|
|
57
|
+
description:
|
|
58
|
+
'Retry a previously failed outbound message. Requires `messageId`. Only failed messages can be retried — calling on a delivered/sent message returns 400.',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
default: 'sendText',
|
|
62
|
+
},
|
|
63
|
+
// --- Account (only for startConversation) ---
|
|
64
|
+
{
|
|
65
|
+
displayName: 'Account',
|
|
66
|
+
name: 'accountId',
|
|
67
|
+
type: 'resourceLocator',
|
|
68
|
+
default: { mode: 'list', value: '' },
|
|
69
|
+
required: true,
|
|
70
|
+
displayOptions: { show: { operation: ['startConversation'] } },
|
|
71
|
+
modes: [
|
|
72
|
+
{ displayName: 'From List', name: 'list', type: 'list', typeOptions: { searchable: true } },
|
|
73
|
+
{ displayName: 'By ID', name: 'id', type: 'string' },
|
|
74
|
+
],
|
|
75
|
+
typeOptions: { loadOptionsMethod: 'getAccounts' },
|
|
76
|
+
description: 'WhatsApp account to send from',
|
|
77
|
+
},
|
|
78
|
+
// --- Send Text ---
|
|
79
|
+
{
|
|
80
|
+
displayName: 'Conversation ID',
|
|
81
|
+
name: 'sendConversationId',
|
|
82
|
+
type: 'string',
|
|
83
|
+
default: '',
|
|
84
|
+
required: true,
|
|
85
|
+
displayOptions: { show: { operation: ['sendText', 'sendMedia'] } },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
displayName: 'Body',
|
|
89
|
+
name: 'textBody',
|
|
90
|
+
type: 'string',
|
|
91
|
+
typeOptions: { rows: 4 },
|
|
92
|
+
default: '',
|
|
93
|
+
required: true,
|
|
94
|
+
displayOptions: { show: { operation: ['sendText'] } },
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
displayName: 'Reply To Message ID',
|
|
98
|
+
name: 'replyToMessageId',
|
|
99
|
+
type: 'string',
|
|
100
|
+
default: '',
|
|
101
|
+
displayOptions: { show: { operation: ['sendText', 'sendMedia'] } },
|
|
102
|
+
description: 'Optional — quote an existing message',
|
|
103
|
+
},
|
|
104
|
+
// --- Send Media ---
|
|
105
|
+
{
|
|
106
|
+
displayName: 'Media Type',
|
|
107
|
+
name: 'mediaType',
|
|
108
|
+
type: 'options',
|
|
109
|
+
default: 'image',
|
|
110
|
+
required: true,
|
|
111
|
+
displayOptions: { show: { operation: ['sendMedia'] } },
|
|
112
|
+
options: [
|
|
113
|
+
{ name: 'Image', value: 'image' },
|
|
114
|
+
{ name: 'Video', value: 'video' },
|
|
115
|
+
{ name: 'Document', value: 'document' },
|
|
116
|
+
{ name: 'Audio', value: 'audio' },
|
|
117
|
+
{ name: 'Voice', value: 'voice' },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
displayName: 'Media URL',
|
|
122
|
+
name: 'mediaUrl',
|
|
123
|
+
type: 'string',
|
|
124
|
+
default: '',
|
|
125
|
+
required: true,
|
|
126
|
+
displayOptions: { show: { operation: ['sendMedia'] } },
|
|
127
|
+
description: 'Public URL of the media file',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
displayName: 'Media Caption',
|
|
131
|
+
name: 'mediaCaption',
|
|
132
|
+
type: 'string',
|
|
133
|
+
default: '',
|
|
134
|
+
displayOptions: { show: { operation: ['sendMedia'] } },
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
displayName: 'Media Filename',
|
|
138
|
+
name: 'mediaFilename',
|
|
139
|
+
type: 'string',
|
|
140
|
+
default: '',
|
|
141
|
+
displayOptions: {
|
|
142
|
+
show: { operation: ['sendMedia'], mediaType: ['document', 'audio', 'voice'] },
|
|
143
|
+
},
|
|
144
|
+
description: 'Required for document/audio/voice; controls how the file is named on receipt.',
|
|
145
|
+
},
|
|
146
|
+
// --- Start Conversation ---
|
|
147
|
+
{
|
|
148
|
+
displayName: 'Phone Number (E.164)',
|
|
149
|
+
name: 'phoneNumber',
|
|
150
|
+
type: 'string',
|
|
151
|
+
default: '',
|
|
152
|
+
required: true,
|
|
153
|
+
displayOptions: { show: { operation: ['startConversation'] } },
|
|
154
|
+
placeholder: '+201234567890',
|
|
155
|
+
description: 'Customer phone in E.164 format',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
displayName: 'Template Name',
|
|
159
|
+
name: 'templateName',
|
|
160
|
+
type: 'string',
|
|
161
|
+
default: '',
|
|
162
|
+
required: true,
|
|
163
|
+
displayOptions: { show: { operation: ['startConversation'] } },
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
displayName: 'Template Language',
|
|
167
|
+
name: 'templateLanguage',
|
|
168
|
+
type: 'string',
|
|
169
|
+
default: 'en',
|
|
170
|
+
displayOptions: { show: { operation: ['startConversation'] } },
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
displayName: 'Template Components (JSON)',
|
|
174
|
+
name: 'templateComponents',
|
|
175
|
+
type: 'json',
|
|
176
|
+
default: '[]',
|
|
177
|
+
displayOptions: { show: { operation: ['startConversation'] } },
|
|
178
|
+
description:
|
|
179
|
+
'Optional JSON array matching Meta template components. Example: `[{"type":"body","parameters":[{"type":"text","text":"Order #12345"}]}]`.',
|
|
180
|
+
},
|
|
181
|
+
// --- List Messages ---
|
|
182
|
+
{
|
|
183
|
+
displayName: 'Conversation ID',
|
|
184
|
+
name: 'listConversationId',
|
|
185
|
+
type: 'string',
|
|
186
|
+
default: '',
|
|
187
|
+
required: true,
|
|
188
|
+
displayOptions: { show: { operation: ['listMessages'] } },
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
displayName: 'Cursor',
|
|
192
|
+
name: 'cursor',
|
|
193
|
+
type: 'string',
|
|
194
|
+
default: '',
|
|
195
|
+
displayOptions: { show: { operation: ['listMessages'] } },
|
|
196
|
+
description: 'Opaque cursor from previous page response',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
displayName: 'Limit',
|
|
200
|
+
name: 'messagesLimit',
|
|
201
|
+
type: 'number',
|
|
202
|
+
default: 50,
|
|
203
|
+
typeOptions: { minValue: 1, maxValue: 100 },
|
|
204
|
+
displayOptions: { show: { operation: ['listMessages'] } },
|
|
205
|
+
},
|
|
206
|
+
// --- Retry ---
|
|
207
|
+
{
|
|
208
|
+
displayName: 'Message ID',
|
|
209
|
+
name: 'messageId',
|
|
210
|
+
type: 'string',
|
|
211
|
+
default: '',
|
|
212
|
+
required: true,
|
|
213
|
+
displayOptions: { show: { operation: ['retry'] } },
|
|
214
|
+
},
|
|
215
|
+
// --- Common ---
|
|
216
|
+
{
|
|
217
|
+
displayName: 'Integration Label',
|
|
218
|
+
name: 'integrationLabel',
|
|
219
|
+
type: 'string',
|
|
220
|
+
default: '',
|
|
221
|
+
displayOptions: { show: { operation: ['sendText', 'sendMedia', 'startConversation'] } },
|
|
222
|
+
description:
|
|
223
|
+
'Optional human-readable label shown in inbox (max 64 chars). Example: "n8n: Order Confirmation".',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
displayName: 'Idempotency Key (X-Request-Id)',
|
|
227
|
+
name: 'requestId',
|
|
228
|
+
type: 'string',
|
|
229
|
+
default: '',
|
|
230
|
+
displayOptions: { show: { operation: ['sendText', 'sendMedia', 'startConversation', 'retry'] } },
|
|
231
|
+
description:
|
|
232
|
+
'Optional UUID — replay-safe debits. Replays with the same id return the cached debit without re-charging.',
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
usableAsTool: true,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
methods = {
|
|
239
|
+
loadOptions: {
|
|
240
|
+
async getAccounts(this: any) {
|
|
241
|
+
return await loadAccountOptions(this)
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async execute(this: any) {
|
|
247
|
+
const items = this.getInputData()
|
|
248
|
+
const returnData: any[] = []
|
|
249
|
+
const resolveAccountId = (i: number) => {
|
|
250
|
+
const v = this.getNodeParameter('accountId', i, '') as any
|
|
251
|
+
if (!v) return undefined
|
|
252
|
+
return typeof v === 'string' ? v : v?.value
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (let i = 0; i < items.length; i++) {
|
|
256
|
+
const operation = this.getNodeParameter('operation', i) as string
|
|
257
|
+
const extraHeaders: Record<string, string> = {}
|
|
258
|
+
const requestId = (this.getNodeParameter('requestId', i, '') as string).trim()
|
|
259
|
+
if (requestId) extraHeaders['X-Request-Id'] = requestId
|
|
260
|
+
|
|
261
|
+
if (operation === 'sendText') {
|
|
262
|
+
const conversationId = this.getNodeParameter('sendConversationId', i) as string
|
|
263
|
+
const body = this.getNodeParameter('textBody', i) as string
|
|
264
|
+
const replyToMessageId = (this.getNodeParameter('replyToMessageId', i, '') as string) || undefined
|
|
265
|
+
const integrationLabel = (this.getNodeParameter('integrationLabel', i, '') as string) || undefined
|
|
266
|
+
const payload: Record<string, any> = { conversationId, type: 'text', body }
|
|
267
|
+
if (replyToMessageId) payload.replyToMessageId = replyToMessageId
|
|
268
|
+
if (integrationLabel) payload.integrationLabel = integrationLabel
|
|
269
|
+
|
|
270
|
+
const res = await confirmxApiRequest<{ message: any }>(this, {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
endpoint: '/messages/send',
|
|
273
|
+
body: payload,
|
|
274
|
+
headers: extraHeaders,
|
|
275
|
+
})
|
|
276
|
+
returnData.push({ json: res.message })
|
|
277
|
+
} else if (operation === 'sendMedia') {
|
|
278
|
+
const conversationId = this.getNodeParameter('sendConversationId', i) as string
|
|
279
|
+
const mediaType = this.getNodeParameter('mediaType', i) as string
|
|
280
|
+
const mediaUrl = this.getNodeParameter('mediaUrl', i) as string
|
|
281
|
+
const mediaCaption = (this.getNodeParameter('mediaCaption', i, '') as string) || undefined
|
|
282
|
+
const mediaFilename = (this.getNodeParameter('mediaFilename', i, '') as string) || undefined
|
|
283
|
+
const replyToMessageId = (this.getNodeParameter('replyToMessageId', i, '') as string) || undefined
|
|
284
|
+
const integrationLabel = (this.getNodeParameter('integrationLabel', i, '') as string) || undefined
|
|
285
|
+
const payload: Record<string, any> = {
|
|
286
|
+
conversationId,
|
|
287
|
+
type: mediaType,
|
|
288
|
+
mediaUrl,
|
|
289
|
+
}
|
|
290
|
+
if (mediaCaption) payload.mediaCaption = mediaCaption
|
|
291
|
+
if (mediaFilename) payload.mediaFilename = mediaFilename
|
|
292
|
+
if (replyToMessageId) payload.replyToMessageId = replyToMessageId
|
|
293
|
+
if (integrationLabel) payload.integrationLabel = integrationLabel
|
|
294
|
+
|
|
295
|
+
const res = await confirmxApiRequest<{ message: any }>(this, {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
endpoint: '/messages/send',
|
|
298
|
+
body: payload,
|
|
299
|
+
headers: extraHeaders,
|
|
300
|
+
})
|
|
301
|
+
returnData.push({ json: res.message })
|
|
302
|
+
} else if (operation === 'startConversation') {
|
|
303
|
+
const accountId = resolveAccountId(i) as string
|
|
304
|
+
const phoneNumber = this.getNodeParameter('phoneNumber', i) as string
|
|
305
|
+
const templateName = this.getNodeParameter('templateName', i) as string
|
|
306
|
+
const templateLanguage = (this.getNodeParameter('templateLanguage', i) as string) || 'en'
|
|
307
|
+
const templateComponentsRaw = (this.getNodeParameter('templateComponents', i, '[]') as string) || '[]'
|
|
308
|
+
const integrationLabel = (this.getNodeParameter('integrationLabel', i, '') as string) || undefined
|
|
309
|
+
|
|
310
|
+
let templateComponents: any[] = []
|
|
311
|
+
try {
|
|
312
|
+
const parsed = JSON.parse(templateComponentsRaw)
|
|
313
|
+
if (Array.isArray(parsed) && parsed.length) templateComponents = parsed
|
|
314
|
+
} catch {
|
|
315
|
+
throw new Error('Template Components must be valid JSON array')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const payload: Record<string, any> = {
|
|
319
|
+
accountId,
|
|
320
|
+
phoneNumber,
|
|
321
|
+
templateName,
|
|
322
|
+
templateLanguage,
|
|
323
|
+
}
|
|
324
|
+
if (templateComponents.length) payload.templateComponents = templateComponents
|
|
325
|
+
if (integrationLabel) payload.integrationLabel = integrationLabel
|
|
326
|
+
|
|
327
|
+
const res = await confirmxApiRequest<{ conversation: any; message: any }>(this, {
|
|
328
|
+
method: 'POST',
|
|
329
|
+
endpoint: '/messages/start-conversation',
|
|
330
|
+
body: payload,
|
|
331
|
+
headers: extraHeaders,
|
|
332
|
+
})
|
|
333
|
+
returnData.push({ json: { conversation: res.conversation, message: res.message } })
|
|
334
|
+
} else if (operation === 'listMessages') {
|
|
335
|
+
const conversationId = this.getNodeParameter('listConversationId', i) as string
|
|
336
|
+
const cursor = (this.getNodeParameter('cursor', i, '') as string) || undefined
|
|
337
|
+
const limit = (this.getNodeParameter('messagesLimit', i) as number) || 50
|
|
338
|
+
const qs: Record<string, any> = { limit }
|
|
339
|
+
if (cursor) qs.cursor = cursor
|
|
340
|
+
const res = await confirmxApiRequest<{ messages: any[]; nextCursor: string | null }>(this, {
|
|
341
|
+
method: 'GET',
|
|
342
|
+
endpoint: `/conversations/${conversationId}/messages`,
|
|
343
|
+
qs,
|
|
344
|
+
})
|
|
345
|
+
for (const m of res.messages || []) {
|
|
346
|
+
returnData.push({ json: m })
|
|
347
|
+
}
|
|
348
|
+
} else if (operation === 'retry') {
|
|
349
|
+
const messageId = this.getNodeParameter('messageId', i) as string
|
|
350
|
+
const res = await confirmxApiRequest<{ ok?: boolean; message?: any }>(this, {
|
|
351
|
+
method: 'POST',
|
|
352
|
+
endpoint: `/messages/${messageId}/retry`,
|
|
353
|
+
headers: extraHeaders,
|
|
354
|
+
})
|
|
355
|
+
returnData.push({ json: { messageId, ...(res as any) } })
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return [returnData]
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfirmXShippingZone = void 0;
|
|
4
|
+
const http_1 = require("../../transports/http");
|
|
5
|
+
class ConfirmXShippingZone {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.description = {
|
|
8
|
+
displayName: 'ConfirmX Shipping Zone',
|
|
9
|
+
name: 'confirmXShippingZone',
|
|
10
|
+
icon: 'file:confirmx.svg',
|
|
11
|
+
group: ['transform'],
|
|
12
|
+
version: 1,
|
|
13
|
+
subtitle: '={{$parameter["operation"]}}',
|
|
14
|
+
description: 'Fuzzy-match an Egyptian governorate + optional area against the ConfirmX shipping-zone taxonomy (10k+ rows, Arabic + English, aliases). Useful for extracting shipping zones from free-text customer input.',
|
|
15
|
+
defaults: { name: 'ConfirmX Shipping Zone' },
|
|
16
|
+
inputs: ['main'],
|
|
17
|
+
outputs: ['main'],
|
|
18
|
+
credentials: [{ name: 'confirmXApi', required: true }],
|
|
19
|
+
properties: [
|
|
20
|
+
{
|
|
21
|
+
displayName: 'Operation',
|
|
22
|
+
name: 'operation',
|
|
23
|
+
type: 'options',
|
|
24
|
+
noDataExpression: true,
|
|
25
|
+
options: [
|
|
26
|
+
{
|
|
27
|
+
name: 'Match',
|
|
28
|
+
value: 'match',
|
|
29
|
+
action: 'Fuzzy-match a shipping zone',
|
|
30
|
+
description: "Fuzzy-match an Egyptian governorate and (optionally) area from free-text customer input (Arabic or English, with typos / aliases tolerated). Returns the matched governorate + area with confidence scores and `externalId`s suitable for writing back to a shipping platform. If `area` is omitted, only the governorate is matched. On success returns `{governorate, area}`; on governorate-not-found returns `suggestions`; on ambiguous area returns `options[]` — present those options to the user and call again with the chosen governorate. Best for Egyptian e-commerce flows.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'List Governorates',
|
|
34
|
+
value: 'list',
|
|
35
|
+
action: 'List governorates and areas',
|
|
36
|
+
description: 'Return the full active governorate→areas tree. Reference data for orgs shipping to Egypt.',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
default: 'match',
|
|
40
|
+
},
|
|
41
|
+
// --- Match parameters ---
|
|
42
|
+
{
|
|
43
|
+
displayName: 'Governorate',
|
|
44
|
+
name: 'governorate',
|
|
45
|
+
type: 'string',
|
|
46
|
+
default: '',
|
|
47
|
+
required: true,
|
|
48
|
+
displayOptions: { show: { operation: ['match'] } },
|
|
49
|
+
description: 'Free-text governorate name. Examples: "Cairo", "القاهرة", "Giza". Typos and Arabic/English mixing are tolerated.',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
displayName: 'Area',
|
|
53
|
+
name: 'area',
|
|
54
|
+
type: 'string',
|
|
55
|
+
default: '',
|
|
56
|
+
displayOptions: { show: { operation: ['match'] } },
|
|
57
|
+
description: 'Optional free-text area name (e.g. "Maadi", "المعادي"). If omitted, only the governorate is matched.',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
usableAsTool: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async execute() {
|
|
64
|
+
const items = this.getInputData();
|
|
65
|
+
const returnData = [];
|
|
66
|
+
for (let i = 0; i < items.length; i++) {
|
|
67
|
+
const operation = this.getNodeParameter('operation', i);
|
|
68
|
+
if (operation === 'match') {
|
|
69
|
+
const governorate = this.getNodeParameter('governorate', i);
|
|
70
|
+
const areaRaw = this.getNodeParameter('area', i) || '';
|
|
71
|
+
const body = { governorate };
|
|
72
|
+
if (areaRaw.trim())
|
|
73
|
+
body.area = areaRaw.trim();
|
|
74
|
+
const res = await (0, http_1.confirmxApiRequest)(this, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
endpoint: '/shipping-zones/match',
|
|
77
|
+
body,
|
|
78
|
+
});
|
|
79
|
+
returnData.push({ json: res });
|
|
80
|
+
}
|
|
81
|
+
else if (operation === 'list') {
|
|
82
|
+
const res = await (0, http_1.confirmxApiRequest)(this, {
|
|
83
|
+
method: 'GET',
|
|
84
|
+
endpoint: '/shipping-zones',
|
|
85
|
+
});
|
|
86
|
+
const governorates = res.governorates || [];
|
|
87
|
+
// Emit one item per governorate so downstream nodes can iterate easily.
|
|
88
|
+
for (const g of governorates) {
|
|
89
|
+
returnData.push({ json: g });
|
|
90
|
+
}
|
|
91
|
+
if (governorates.length === 0) {
|
|
92
|
+
returnData.push({ json: { governorates: [] } });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return [returnData];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.ConfirmXShippingZone = ConfirmXShippingZone;
|
|
100
|
+
//# sourceMappingURL=ConfirmXShippingZone.node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfirmXShippingZone.node.js","sourceRoot":"","sources":["ConfirmXShippingZone.node.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow'
|
|
2
|
+
import { confirmxApiRequest } from '../../transports/http'
|
|
3
|
+
|
|
4
|
+
export class ConfirmXShippingZone implements INodeType {
|
|
5
|
+
description: INodeTypeDescription = {
|
|
6
|
+
displayName: 'ConfirmX Shipping Zone',
|
|
7
|
+
name: 'confirmXShippingZone',
|
|
8
|
+
icon: 'file:confirmx.svg',
|
|
9
|
+
group: ['transform'],
|
|
10
|
+
version: 1,
|
|
11
|
+
subtitle: '={{$parameter["operation"]}}',
|
|
12
|
+
description:
|
|
13
|
+
'Fuzzy-match an Egyptian governorate + optional area against the ConfirmX shipping-zone taxonomy (10k+ rows, Arabic + English, aliases). Useful for extracting shipping zones from free-text customer input.',
|
|
14
|
+
defaults: { name: 'ConfirmX Shipping Zone' },
|
|
15
|
+
inputs: ['main'],
|
|
16
|
+
outputs: ['main'],
|
|
17
|
+
credentials: [{ name: 'confirmXApi', required: true }],
|
|
18
|
+
properties: [
|
|
19
|
+
{
|
|
20
|
+
displayName: 'Operation',
|
|
21
|
+
name: 'operation',
|
|
22
|
+
type: 'options',
|
|
23
|
+
noDataExpression: true,
|
|
24
|
+
options: [
|
|
25
|
+
{
|
|
26
|
+
name: 'Match',
|
|
27
|
+
value: 'match',
|
|
28
|
+
action: 'Fuzzy-match a shipping zone',
|
|
29
|
+
description:
|
|
30
|
+
"Fuzzy-match an Egyptian governorate and (optionally) area from free-text customer input (Arabic or English, with typos / aliases tolerated). Returns the matched governorate + area with confidence scores and `externalId`s suitable for writing back to a shipping platform. If `area` is omitted, only the governorate is matched. On success returns `{governorate, area}`; on governorate-not-found returns `suggestions`; on ambiguous area returns `options[]` — present those options to the user and call again with the chosen governorate. Best for Egyptian e-commerce flows.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'List Governorates',
|
|
34
|
+
value: 'list',
|
|
35
|
+
action: 'List governorates and areas',
|
|
36
|
+
description:
|
|
37
|
+
'Return the full active governorate→areas tree. Reference data for orgs shipping to Egypt.',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
default: 'match',
|
|
41
|
+
},
|
|
42
|
+
// --- Match parameters ---
|
|
43
|
+
{
|
|
44
|
+
displayName: 'Governorate',
|
|
45
|
+
name: 'governorate',
|
|
46
|
+
type: 'string',
|
|
47
|
+
default: '',
|
|
48
|
+
required: true,
|
|
49
|
+
displayOptions: { show: { operation: ['match'] } },
|
|
50
|
+
description:
|
|
51
|
+
'Free-text governorate name. Examples: "Cairo", "القاهرة", "Giza". Typos and Arabic/English mixing are tolerated.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
displayName: 'Area',
|
|
55
|
+
name: 'area',
|
|
56
|
+
type: 'string',
|
|
57
|
+
default: '',
|
|
58
|
+
displayOptions: { show: { operation: ['match'] } },
|
|
59
|
+
description:
|
|
60
|
+
'Optional free-text area name (e.g. "Maadi", "المعادي"). If omitted, only the governorate is matched.',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
usableAsTool: true,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async execute(this: any) {
|
|
67
|
+
const items = this.getInputData()
|
|
68
|
+
const returnData: any[] = []
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < items.length; i++) {
|
|
71
|
+
const operation = this.getNodeParameter('operation', i) as string
|
|
72
|
+
|
|
73
|
+
if (operation === 'match') {
|
|
74
|
+
const governorate = this.getNodeParameter('governorate', i) as string
|
|
75
|
+
const areaRaw = (this.getNodeParameter('area', i) as string) || ''
|
|
76
|
+
const body: Record<string, any> = { governorate }
|
|
77
|
+
if (areaRaw.trim()) body.area = areaRaw.trim()
|
|
78
|
+
|
|
79
|
+
const res = await confirmxApiRequest<any>(this, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
endpoint: '/shipping-zones/match',
|
|
82
|
+
body,
|
|
83
|
+
})
|
|
84
|
+
returnData.push({ json: res })
|
|
85
|
+
} else if (operation === 'list') {
|
|
86
|
+
const res = await confirmxApiRequest<any>(this, {
|
|
87
|
+
method: 'GET',
|
|
88
|
+
endpoint: '/shipping-zones',
|
|
89
|
+
})
|
|
90
|
+
const governorates = res.governorates || []
|
|
91
|
+
// Emit one item per governorate so downstream nodes can iterate easily.
|
|
92
|
+
for (const g of governorates) {
|
|
93
|
+
returnData.push({ json: g })
|
|
94
|
+
}
|
|
95
|
+
if (governorates.length === 0) {
|
|
96
|
+
returnData.push({ json: { governorates: [] } })
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return [returnData]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class ConfirmXTemplate implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
methods: {
|
|
5
|
+
loadOptions: {
|
|
6
|
+
getAccounts(this: any): Promise<{
|
|
7
|
+
name: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}[]>;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
execute(this: any): Promise<any[][]>;
|
|
13
|
+
}
|