backend-manager 5.0.147 → 5.0.149
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/CHANGELOG.md +58 -0
- package/CLAUDE.md +26 -0
- package/package.json +1 -1
- package/src/cli/commands/emulator.js +14 -4
- package/src/cli/commands/test.js +4 -10
- package/src/manager/cron/daily/ghostii-auto-publisher.js +25 -25
- package/src/manager/cron/frequent/abandoned-carts.js +7 -5
- package/src/manager/cron/frequent/email-queue.js +56 -0
- package/src/manager/events/auth/before-signin.js +3 -0
- package/src/manager/events/auth/on-delete.js +8 -0
- package/src/manager/events/firestore/payments-disputes/on-write.js +2 -1
- package/src/manager/events/firestore/payments-webhooks/on-write.js +9 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +7 -21
- package/src/manager/functions/core/actions/api/admin/get-stats.js +2 -2
- package/src/manager/functions/core/actions/api/admin/send-email.js +14 -14
- package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +22 -318
- package/src/manager/functions/core/actions/api/general/emails/general:download-app-link.js +1 -1
- package/src/manager/functions/core/actions/api/general/remove-marketing-contact.js +2 -185
- package/src/manager/functions/core/actions/api/general/send-email.js +1 -1
- package/src/manager/functions/core/actions/api/special/setup-electron-manager-client.js +2 -2
- package/src/manager/functions/core/actions/api/test/health.js +1 -0
- package/src/manager/helpers/api-manager.js +2 -2
- package/src/manager/helpers/user.js +3 -1
- package/src/manager/index.js +15 -10
- package/src/manager/libraries/email/constants.js +243 -0
- package/src/manager/libraries/email/index.js +145 -0
- package/src/manager/libraries/email/marketing/index.js +377 -0
- package/src/manager/libraries/email/providers/beehiiv.js +258 -0
- package/src/manager/libraries/email/providers/sendgrid.js +429 -0
- package/src/manager/libraries/{email.js → email/transactional/index.js} +91 -99
- package/src/manager/libraries/email/validation.js +168 -0
- package/src/manager/libraries/infer-contact.js +1 -1
- package/src/manager/routes/admin/cron/post.js +3 -3
- package/src/manager/routes/admin/email/post.js +1 -1
- package/src/manager/routes/admin/stats/get.js +2 -2
- package/src/manager/routes/{app → brand}/get.js +1 -1
- package/src/manager/routes/general/email/templates/download-app-link.js +1 -1
- package/src/manager/routes/marketing/contact/delete.js +2 -164
- package/src/manager/routes/marketing/contact/post.js +45 -298
- package/src/manager/routes/marketing/contact/put.js +39 -0
- package/src/manager/routes/payments/cancel/post.js +11 -0
- package/src/manager/routes/special/electron-client/post.js +3 -3
- package/src/manager/routes/test/health/get.js +1 -0
- package/src/manager/routes/user/data-request/delete.js +2 -2
- package/src/manager/routes/user/data-request/get.js +2 -2
- package/src/manager/routes/user/data-request/post.js +2 -2
- package/src/manager/routes/user/delete.js +1 -1
- package/src/manager/routes/user/feedback/post.js +12 -8
- package/src/manager/routes/user/signup/post.js +48 -37
- package/src/manager/schemas/admin/email/post.js +4 -4
- package/src/manager/schemas/marketing/contact/delete.js +3 -1
- package/src/manager/schemas/marketing/contact/post.js +3 -1
- package/src/manager/schemas/marketing/contact/put.js +6 -0
- package/src/manager/schemas/special/electron-client/post.js +2 -2
- package/src/manager/schemas/user/feedback/post.js +2 -2
- package/src/test/run-tests.js +1 -1
- package/src/test/runner.js +22 -10
- package/src/test/test-accounts.js +9 -0
- package/src/test/utils/extended-mode-warning.js +11 -0
- package/templates/_.env +1 -0
- package/test/events/payments/journey-payments-cancel-endpoint.js +11 -0
- package/test/events/payments/journey-payments-trial-cancel.js +11 -0
- package/test/functions/admin/edit-post.js +2 -2
- package/test/functions/admin/write-repo-content.js +2 -2
- package/test/functions/general/add-marketing-contact.js +21 -23
- package/test/helpers/email-validation.js +420 -0
- package/test/helpers/email.js +119 -6
- package/test/helpers/marketing-lifecycle.js +121 -0
- package/test/helpers/user.js +2 -2
- package/test/routes/admin/create-post.js +2 -2
- package/test/routes/admin/post.js +2 -2
- package/test/routes/admin/repo-content.js +2 -2
- package/test/routes/marketing/contact.js +21 -24
- package/test/routes/payments/cancel.js +18 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SendGrid provider — shared API helpers for contacts and Single Sends
|
|
3
|
+
*
|
|
4
|
+
* Used by: marketing/index.js (sync, remove, campaigns)
|
|
5
|
+
*/
|
|
6
|
+
const fetch = require('wonderful-fetch');
|
|
7
|
+
const Manager = require('../../../index.js');
|
|
8
|
+
const { resolveFieldValues } = require('../constants.js');
|
|
9
|
+
|
|
10
|
+
const BASE_URL = 'https://api.sendgrid.com/v3';
|
|
11
|
+
|
|
12
|
+
// --- Internal helpers ---
|
|
13
|
+
|
|
14
|
+
function headers() {
|
|
15
|
+
return {
|
|
16
|
+
'Authorization': `Bearer ${process.env.SENDGRID_API_KEY}`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Cached field name → SendGrid ID map (e.g., { brand_id: 'e1_T', user_auth_uid: 'e2_T' })
|
|
21
|
+
let _fieldIdCache = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fetch custom field definitions from SendGrid and build a name → id map.
|
|
25
|
+
* Cached in memory for the lifetime of the process.
|
|
26
|
+
*
|
|
27
|
+
* @returns {object} Map of field name → SendGrid field ID
|
|
28
|
+
*/
|
|
29
|
+
async function resolveFieldIds() {
|
|
30
|
+
if (_fieldIdCache) {
|
|
31
|
+
return _fieldIdCache;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const data = await fetch(`${BASE_URL}/marketing/field_definitions`, {
|
|
36
|
+
response: 'json',
|
|
37
|
+
headers: headers(),
|
|
38
|
+
timeout: 10000,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
_fieldIdCache = {};
|
|
42
|
+
|
|
43
|
+
for (const field of (data.custom_fields || [])) {
|
|
44
|
+
_fieldIdCache[field.name] = field.id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return _fieldIdCache;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error('SendGrid resolveFieldIds error:', e);
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Contact Management ---
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Upsert contacts to SendGrid Marketing Contacts.
|
|
58
|
+
* Creates if new, merges/overwrites fields if existing.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} options
|
|
61
|
+
* @param {Array<object>} options.contacts - Array of contact objects ({ email, first_name, last_name, custom_fields })
|
|
62
|
+
* @param {Array<string>} [options.listIds] - List IDs to add contacts to
|
|
63
|
+
* @returns {{ success: boolean, jobId?: string, error?: string }}
|
|
64
|
+
*/
|
|
65
|
+
async function upsertContacts({ contacts, listIds }) {
|
|
66
|
+
try {
|
|
67
|
+
const body = { contacts };
|
|
68
|
+
|
|
69
|
+
if (listIds && listIds.length) {
|
|
70
|
+
body.list_ids = listIds;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const data = await fetch(`${BASE_URL}/marketing/contacts`, {
|
|
74
|
+
method: 'put',
|
|
75
|
+
response: 'json',
|
|
76
|
+
headers: headers(),
|
|
77
|
+
timeout: 15000,
|
|
78
|
+
body,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (data.job_id) {
|
|
82
|
+
return { success: true, jobId: data.job_id };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { success: false, error: data.errors?.[0]?.message || 'Unknown error' };
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error('SendGrid upsertContacts error:', e);
|
|
88
|
+
return { success: false, error: e.message };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remove a contact from SendGrid by email address.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} email
|
|
96
|
+
* @returns {{ success: boolean, jobId?: string, skipped?: boolean, error?: string }}
|
|
97
|
+
*/
|
|
98
|
+
async function removeContact(email) {
|
|
99
|
+
try {
|
|
100
|
+
// Step 1: Get contact ID by email
|
|
101
|
+
const searchData = await fetch(`${BASE_URL}/marketing/contacts/search/emails`, {
|
|
102
|
+
method: 'post',
|
|
103
|
+
response: 'json',
|
|
104
|
+
headers: headers(),
|
|
105
|
+
timeout: 10000,
|
|
106
|
+
body: { emails: [email] },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (!searchData.result?.[email]?.contact?.id) {
|
|
110
|
+
return { success: true, skipped: true, reason: 'Contact not found' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const contactId = searchData.result[email].contact.id;
|
|
114
|
+
|
|
115
|
+
// Step 2: Delete contact by ID
|
|
116
|
+
const deleteData = await fetch(`${BASE_URL}/marketing/contacts?ids=${contactId}`, {
|
|
117
|
+
method: 'delete',
|
|
118
|
+
response: 'json',
|
|
119
|
+
headers: headers(),
|
|
120
|
+
timeout: 10000,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (deleteData.job_id) {
|
|
124
|
+
return { success: true, jobId: deleteData.job_id };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { success: false, error: deleteData.errors?.[0]?.message || 'Delete failed' };
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error('SendGrid removeContact error:', e);
|
|
130
|
+
return { success: false, error: e.message };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get a SendGrid list ID by brand name (fuzzy match).
|
|
136
|
+
*
|
|
137
|
+
* @param {string} brandName
|
|
138
|
+
* @returns {string|null} List ID or null
|
|
139
|
+
*/
|
|
140
|
+
async function getListId() {
|
|
141
|
+
const brandName = Manager.config.brand?.name;
|
|
142
|
+
const brandNameLower = (brandName || '').toLowerCase();
|
|
143
|
+
const allLists = [];
|
|
144
|
+
let pageToken = '';
|
|
145
|
+
const pageSize = 1000;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
while (true) {
|
|
149
|
+
const url = `${BASE_URL}/marketing/lists?page_size=${pageSize}${pageToken ? `&page_token=${pageToken}` : ''}`;
|
|
150
|
+
const data = await fetch(url, {
|
|
151
|
+
response: 'json',
|
|
152
|
+
headers: headers(),
|
|
153
|
+
timeout: 10000,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!data.result || data.result.length === 0) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const matchedList = data.result.find(list =>
|
|
161
|
+
list.name.toLowerCase() === brandNameLower
|
|
162
|
+
|| list.name.toLowerCase().includes(brandNameLower)
|
|
163
|
+
|| brandNameLower.includes(list.name.toLowerCase())
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (matchedList) {
|
|
167
|
+
return matchedList.id;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
allLists.push(...data.result);
|
|
171
|
+
|
|
172
|
+
if (!data._metadata?.next) {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const nextUrl = new URL(data._metadata.next);
|
|
177
|
+
pageToken = nextUrl.searchParams.get('page_token');
|
|
178
|
+
|
|
179
|
+
if (!pageToken) {
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (allLists.length === 1) {
|
|
185
|
+
return allLists[0].id;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (allLists.length > 0) {
|
|
189
|
+
console.error(`SendGrid: No list matched brand "${brandName}". Available: ${allLists.map(l => l.name).join(', ')}`);
|
|
190
|
+
}
|
|
191
|
+
} catch (e) {
|
|
192
|
+
console.error('SendGrid list lookup error:', e);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- Single Sends (Campaigns) ---
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Create a Single Send (marketing campaign).
|
|
202
|
+
*
|
|
203
|
+
* @param {object} options
|
|
204
|
+
* @param {string} options.name - Campaign name
|
|
205
|
+
* @param {string} options.subject - Email subject
|
|
206
|
+
* @param {string} options.templateId - SendGrid template ID
|
|
207
|
+
* @param {object} options.from - { email, name }
|
|
208
|
+
* @param {object} options.sendTo - { list_ids?, segment_ids?, all? }
|
|
209
|
+
* @param {number} [options.asmGroupId] - Unsubscribe group ID
|
|
210
|
+
* @param {Array<string>} [options.categories] - Email categories
|
|
211
|
+
* @param {object} [options.dynamicTemplateData] - Template variables
|
|
212
|
+
* @returns {{ success: boolean, id?: string, error?: string }}
|
|
213
|
+
*/
|
|
214
|
+
async function createSingleSend({ name, subject, templateId, from, sendTo, asmGroupId, categories, dynamicTemplateData }) {
|
|
215
|
+
try {
|
|
216
|
+
const body = {
|
|
217
|
+
name,
|
|
218
|
+
send_to: sendTo,
|
|
219
|
+
email_config: {
|
|
220
|
+
subject,
|
|
221
|
+
sender_id: null,
|
|
222
|
+
custom_unsubscribe_url: null,
|
|
223
|
+
generate_plain_content: true,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Use design_editor with template
|
|
228
|
+
if (templateId) {
|
|
229
|
+
body.email_config.editor = 'design';
|
|
230
|
+
body.email_config.template_id = templateId;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (from) {
|
|
234
|
+
body.email_config.sender_id = null;
|
|
235
|
+
// SendGrid Single Sends use sender_id OR from, depending on account setup.
|
|
236
|
+
// We'll set the from fields directly if supported, otherwise the sender_id
|
|
237
|
+
// must be pre-configured in SendGrid.
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (asmGroupId) {
|
|
241
|
+
body.email_config.suppression_group_id = asmGroupId;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (categories && categories.length) {
|
|
245
|
+
body.email_config.categories = categories;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const data = await fetch(`${BASE_URL}/marketing/singlesends`, {
|
|
249
|
+
method: 'post',
|
|
250
|
+
response: 'json',
|
|
251
|
+
headers: headers(),
|
|
252
|
+
timeout: 15000,
|
|
253
|
+
body,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (data.id) {
|
|
257
|
+
return { success: true, id: data.id };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { success: false, error: data.errors?.[0]?.message || 'Unknown error' };
|
|
261
|
+
} catch (e) {
|
|
262
|
+
console.error('SendGrid createSingleSend error:', e);
|
|
263
|
+
return { success: false, error: e.message };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Schedule a Single Send for delivery.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} singleSendId - The Single Send ID
|
|
271
|
+
* @param {string} sendAt - ISO 8601 datetime string (e.g., '2026-04-01T14:00:00Z'), or 'now'
|
|
272
|
+
* @returns {{ success: boolean, error?: string }}
|
|
273
|
+
*/
|
|
274
|
+
async function scheduleSingleSend(singleSendId, sendAt) {
|
|
275
|
+
try {
|
|
276
|
+
const data = await fetch(`${BASE_URL}/marketing/singlesends/${singleSendId}/schedule`, {
|
|
277
|
+
method: 'put',
|
|
278
|
+
response: 'json',
|
|
279
|
+
headers: headers(),
|
|
280
|
+
timeout: 15000,
|
|
281
|
+
body: { send_at: sendAt },
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (data.send_at || data.status === 'scheduled') {
|
|
285
|
+
return { success: true };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { success: false, error: data.errors?.[0]?.message || 'Schedule failed' };
|
|
289
|
+
} catch (e) {
|
|
290
|
+
console.error('SendGrid scheduleSingleSend error:', e);
|
|
291
|
+
return { success: false, error: e.message };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Cancel a scheduled Single Send.
|
|
297
|
+
*
|
|
298
|
+
* @param {string} singleSendId
|
|
299
|
+
* @returns {{ success: boolean, error?: string }}
|
|
300
|
+
*/
|
|
301
|
+
async function cancelSingleSend(singleSendId) {
|
|
302
|
+
try {
|
|
303
|
+
const data = await fetch(`${BASE_URL}/marketing/singlesends/${singleSendId}`, {
|
|
304
|
+
method: 'delete',
|
|
305
|
+
response: 'json',
|
|
306
|
+
headers: headers(),
|
|
307
|
+
timeout: 10000,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return { success: true };
|
|
311
|
+
} catch (e) {
|
|
312
|
+
console.error('SendGrid cancelSingleSend error:', e);
|
|
313
|
+
return { success: false, error: e.message };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get a Single Send by ID.
|
|
319
|
+
*
|
|
320
|
+
* @param {string} singleSendId
|
|
321
|
+
* @returns {object|null}
|
|
322
|
+
*/
|
|
323
|
+
async function getSingleSend(singleSendId) {
|
|
324
|
+
try {
|
|
325
|
+
const data = await fetch(`${BASE_URL}/marketing/singlesends/${singleSendId}`, {
|
|
326
|
+
response: 'json',
|
|
327
|
+
headers: headers(),
|
|
328
|
+
timeout: 10000,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return data.id ? data : null;
|
|
332
|
+
} catch (e) {
|
|
333
|
+
console.error('SendGrid getSingleSend error:', e);
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* List Single Sends with optional status filter.
|
|
340
|
+
*
|
|
341
|
+
* @param {object} [options]
|
|
342
|
+
* @param {string} [options.status] - Filter by status: draft, scheduled, triggered
|
|
343
|
+
* @returns {Array<object>}
|
|
344
|
+
*/
|
|
345
|
+
async function listSingleSends(options) {
|
|
346
|
+
const { status } = options || {};
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const url = `${BASE_URL}/marketing/singlesends${status ? `?status=${status}` : ''}`;
|
|
350
|
+
const data = await fetch(url, {
|
|
351
|
+
response: 'json',
|
|
352
|
+
headers: headers(),
|
|
353
|
+
timeout: 10000,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return data.result || [];
|
|
357
|
+
} catch (e) {
|
|
358
|
+
console.error('SendGrid listSingleSends error:', e);
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Add a contact to SendGrid — resolves list, upserts with optional custom fields.
|
|
365
|
+
*
|
|
366
|
+
* @param {object} options
|
|
367
|
+
* @param {string} options.email
|
|
368
|
+
* @param {string} [options.firstName]
|
|
369
|
+
* @param {string} [options.lastName]
|
|
370
|
+
* @param {object} [options.customFields] - Pre-built custom_fields object (keyed by SendGrid field IDs)
|
|
371
|
+
* @returns {{ success: boolean, jobId?: string, listId?: string, error?: string }}
|
|
372
|
+
*/
|
|
373
|
+
async function addContact({ email, firstName, lastName, customFields }) {
|
|
374
|
+
const contact = {
|
|
375
|
+
email: email.toLowerCase(),
|
|
376
|
+
first_name: firstName || undefined,
|
|
377
|
+
last_name: lastName || undefined,
|
|
378
|
+
custom_fields: customFields || {},
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const listId = await getListId();
|
|
382
|
+
const result = await upsertContacts({
|
|
383
|
+
contacts: [contact],
|
|
384
|
+
listIds: listId ? [listId] : [],
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
if (result.success && listId) {
|
|
388
|
+
result.listId = listId;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Build SendGrid custom_fields object from a user doc.
|
|
396
|
+
* Resolves all field values, then maps field names to SendGrid IDs via runtime lookup.
|
|
397
|
+
*
|
|
398
|
+
* @param {object} userDoc - User document from Firestore
|
|
399
|
+
* @returns {object} Custom fields keyed by SendGrid field ID (e.g., { e1_T: 'basic' })
|
|
400
|
+
*/
|
|
401
|
+
async function buildFields(userDoc) {
|
|
402
|
+
const values = resolveFieldValues(userDoc, Manager.config);
|
|
403
|
+
const idMap = await resolveFieldIds();
|
|
404
|
+
const fields = {};
|
|
405
|
+
|
|
406
|
+
for (const [name, value] of Object.entries(values)) {
|
|
407
|
+
const sgId = idMap[name];
|
|
408
|
+
|
|
409
|
+
if (sgId) {
|
|
410
|
+
fields[sgId] = value;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return fields;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = {
|
|
418
|
+
// Contacts
|
|
419
|
+
addContact,
|
|
420
|
+
removeContact,
|
|
421
|
+
buildFields,
|
|
422
|
+
|
|
423
|
+
// Campaigns (Single Sends)
|
|
424
|
+
createSingleSend,
|
|
425
|
+
scheduleSingleSend,
|
|
426
|
+
cancelSingleSend,
|
|
427
|
+
getSingleSend,
|
|
428
|
+
listSingleSends,
|
|
429
|
+
};
|