backend-manager 5.0.148 → 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 +50 -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/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/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
package/src/manager/index.js
CHANGED
|
@@ -138,6 +138,11 @@ Manager.prototype.init = function (exporter, options) {
|
|
|
138
138
|
(_objValue, srcValue) => isArray(srcValue) ? srcValue : undefined,
|
|
139
139
|
);
|
|
140
140
|
|
|
141
|
+
// Expose config on the constructor for static access by internal libraries.
|
|
142
|
+
// Since Node.js caches require(), any `require('./index.js')` returns this same
|
|
143
|
+
// Manager function with .config already set — no need for setConfig() patterns.
|
|
144
|
+
Manager.config = self.config;
|
|
145
|
+
|
|
141
146
|
// Set PAYPAL_CLIENT_ID from config (clientId is public, not a secret — lives in config, not .env)
|
|
142
147
|
process.env.PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID || self.config?.payment?.processors?.paypal?.clientId || '';
|
|
143
148
|
|
|
@@ -145,12 +150,12 @@ Manager.prototype.init = function (exporter, options) {
|
|
|
145
150
|
process.env.CHARGEBEE_SITE = process.env.CHARGEBEE_SITE || self.config?.payment?.processors?.chargebee?.site || '';
|
|
146
151
|
|
|
147
152
|
// Resolve legacy paths
|
|
148
|
-
// TODO: Remove this in future versions (after
|
|
153
|
+
// TODO: Remove this in future versions (after all consumers migrate to brand.id)
|
|
149
154
|
self.config.app = self.config.app || {};
|
|
150
|
-
self.config.
|
|
155
|
+
self.config.brand.id = self.config.brand.id || self.config.app.id || null;
|
|
151
156
|
|
|
152
|
-
// Get
|
|
153
|
-
const
|
|
157
|
+
// Get brand ID
|
|
158
|
+
const brandId = self.config?.brand?.id;
|
|
154
159
|
|
|
155
160
|
// Set log
|
|
156
161
|
if (options.logSavePath) {
|
|
@@ -267,13 +272,13 @@ Manager.prototype.init = function (exporter, options) {
|
|
|
267
272
|
self.assistant.log('Resolved backendManagerConfigPath', self.project.backendManagerConfigPath);
|
|
268
273
|
}
|
|
269
274
|
|
|
270
|
-
if (!
|
|
271
|
-
self.assistant.warn('⚠️ Missing config.
|
|
275
|
+
if (!brandId) {
|
|
276
|
+
self.assistant.warn('⚠️ Missing config.brand.id');
|
|
272
277
|
}
|
|
273
278
|
|
|
274
279
|
// Setup sentry
|
|
275
280
|
if (self.options.sentry) {
|
|
276
|
-
const sentryRelease = `${
|
|
281
|
+
const sentryRelease = `${brandId || self.project.projectId}@${self.package.version}`;
|
|
277
282
|
const sentryDSN = self.config?.sentry?.dsn || '';
|
|
278
283
|
// self.assistant.log('Sentry', sentryRelease, sentryDSN);
|
|
279
284
|
|
|
@@ -310,8 +315,8 @@ Manager.prototype.init = function (exporter, options) {
|
|
|
310
315
|
// self.app = self.libraries.initializedAdmin;
|
|
311
316
|
|
|
312
317
|
const loadedProjectId = serviceAccount.project_id;
|
|
313
|
-
if (!loadedProjectId || !loadedProjectId.includes(
|
|
314
|
-
self.assistant.error(`Loaded app may have wrong service account: ${loadedProjectId} =/= ${
|
|
318
|
+
if (!loadedProjectId || !loadedProjectId.includes(brandId)) {
|
|
319
|
+
self.assistant.error(`Loaded app may have wrong service account: ${loadedProjectId} =/= ${brandId}`);
|
|
315
320
|
}
|
|
316
321
|
}
|
|
317
322
|
}
|
|
@@ -534,7 +539,7 @@ Manager.prototype.Metadata = function () {
|
|
|
534
539
|
|
|
535
540
|
Manager.prototype.Email = function (assistant) {
|
|
536
541
|
const self = this;
|
|
537
|
-
self.libraries.Email = self.libraries.Email || require('./libraries/email.js');
|
|
542
|
+
self.libraries.Email = self.libraries.Email || require('./libraries/email/index.js');
|
|
538
543
|
return new self.libraries.Email(assistant);
|
|
539
544
|
};
|
|
540
545
|
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for email libraries (transactional + marketing)
|
|
3
|
+
*
|
|
4
|
+
* SSOT for templates, ASM groups, and semantic senders.
|
|
5
|
+
* Used by: transactional/index.js, marketing/index.js, providers/*
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Template shortcut map — callers use readable paths instead of SendGrid IDs
|
|
9
|
+
// Paths mirror the email website structure: {category}/{subcategory}/{name}
|
|
10
|
+
const TEMPLATES = {
|
|
11
|
+
// v2 templates
|
|
12
|
+
'main/basic/card': 'd-1cd2eee44b6340268c964cd7971d49b9',
|
|
13
|
+
'main/engagement/feedback': 'd-319ab5c9d5074b21926a93562d6f41f6',
|
|
14
|
+
'main/misc/app-download-link': 'd-fc8b4834d7e1472896fe7e46152029f4',
|
|
15
|
+
'main/order/confirmation': 'd-5371ac2b4e3b490bbce51bfc2922ece8',
|
|
16
|
+
'main/order/payment-failed': 'd-e56af0ac62364bfb9e50af02854e2cd3',
|
|
17
|
+
'main/order/payment-recovered': 'd-d6dbd17a260a4755b34a852ba09c2454',
|
|
18
|
+
'main/order/cancellation-requested': 'd-78074f3e8c844146bf263b86fc8d5ecf',
|
|
19
|
+
'main/order/cancelled': 'd-39041132e6b24e5ebf0e95bce2d94dba',
|
|
20
|
+
'main/order/plan-changed': 'd-399086311bbb48b4b77bc90b20fb9d0a',
|
|
21
|
+
'main/order/trial-ending': 'd-af8ab499cbfb4d56918b4118f44343b0',
|
|
22
|
+
'main/order/refunded': 'd-aa47fdbffa2b4ca9b73b6256e963e49f',
|
|
23
|
+
'main/order/abandoned-cart': 'd-d8b3fa67e2b44b398dc280d0576bf1b7',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// "default" resolves to the basic card template
|
|
27
|
+
TEMPLATES['default'] = TEMPLATES['main/basic/card'];
|
|
28
|
+
|
|
29
|
+
// Group shortcut map — SendGrid ASM group IDs
|
|
30
|
+
// Rename these in SendGrid dashboard to match the comments
|
|
31
|
+
const GROUPS = {
|
|
32
|
+
'orders': 16223, // BEM - Order Updates
|
|
33
|
+
'hello': 35092, // BEM - Onboarding
|
|
34
|
+
'account': 25927, // BEM - Account
|
|
35
|
+
'marketing': 25928, // BEM - Marketing & Promotions
|
|
36
|
+
'newsletter': 28096, // BEM - Newsletter
|
|
37
|
+
'security': 35093, // BEM - Security
|
|
38
|
+
'internal': 35094, // BEM - Internal Alerts
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Semantic sender categories — pass `sender: 'orders'` to auto-resolve from address, display name, and ASM group
|
|
42
|
+
const SENDERS = {
|
|
43
|
+
// Payment receipts, failed/recovered, cancellation, plan changes, refunds, trial ending
|
|
44
|
+
orders: {
|
|
45
|
+
localPart: 'orders',
|
|
46
|
+
displayName: '{brand} Orders',
|
|
47
|
+
group: GROUPS['orders'],
|
|
48
|
+
},
|
|
49
|
+
// Warm onboarding: welcome, 7-day checkup, feedback request
|
|
50
|
+
hello: {
|
|
51
|
+
localPart: 'hello',
|
|
52
|
+
displayName: '{brand}',
|
|
53
|
+
group: GROUPS['hello'],
|
|
54
|
+
},
|
|
55
|
+
// Transactional account actions: deletion, data requests
|
|
56
|
+
account: {
|
|
57
|
+
localPart: 'account',
|
|
58
|
+
displayName: '{brand} Account',
|
|
59
|
+
group: GROUPS['account'],
|
|
60
|
+
},
|
|
61
|
+
// Promotions, discounts, win-back, abandoned cart, app download link
|
|
62
|
+
marketing: {
|
|
63
|
+
localPart: 'offers',
|
|
64
|
+
displayName: '{brand}',
|
|
65
|
+
group: GROUPS['marketing'],
|
|
66
|
+
},
|
|
67
|
+
// Forgot password, 2FA, password reset
|
|
68
|
+
security: {
|
|
69
|
+
localPart: 'security',
|
|
70
|
+
displayName: '{brand} Security',
|
|
71
|
+
group: GROUPS['security'],
|
|
72
|
+
},
|
|
73
|
+
// Monthly newsletters, feature announcements, industry news
|
|
74
|
+
newsletter: {
|
|
75
|
+
localPart: 'newsletter',
|
|
76
|
+
displayName: '{brand}',
|
|
77
|
+
group: GROUPS['newsletter'],
|
|
78
|
+
},
|
|
79
|
+
// Dispute alerts, system notifications sent to brand contact
|
|
80
|
+
internal: {
|
|
81
|
+
localPart: 'alerts',
|
|
82
|
+
displayName: '{brand} Alerts',
|
|
83
|
+
group: GROUPS['internal'],
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Default marketing providers — SSOT for all provider loops
|
|
88
|
+
const DEFAULT_PROVIDERS = ['sendgrid', 'beehiiv'];
|
|
89
|
+
|
|
90
|
+
// SendGrid limit for scheduled emails (72 hours, but use 71 for buffer)
|
|
91
|
+
const SEND_AT_LIMIT = 71;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Convert SVG image URLs to PNG equivalents — email clients don't render SVGs.
|
|
95
|
+
* CDN naming convention: `-x.svg` -> `-1024.png`
|
|
96
|
+
*/
|
|
97
|
+
function sanitizeImagesForEmail(images) {
|
|
98
|
+
const result = {};
|
|
99
|
+
|
|
100
|
+
for (const [key, value] of Object.entries(images)) {
|
|
101
|
+
if (typeof value === 'string' && value.endsWith('.svg')) {
|
|
102
|
+
result[key] = value.replace(/-x\.svg$/, '-1024.png');
|
|
103
|
+
} else {
|
|
104
|
+
result[key] = value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* URL-encode a value as base64
|
|
113
|
+
*/
|
|
114
|
+
function encode(s) {
|
|
115
|
+
return encodeURIComponent(Buffer.from(String(s)).toString('base64'));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create an Error with a code property for distinguishing build (400) vs send (500) failures.
|
|
120
|
+
*/
|
|
121
|
+
function errorWithCode(message, code) {
|
|
122
|
+
const err = new Error(message);
|
|
123
|
+
err.code = code;
|
|
124
|
+
return err;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Master field dictionary — the key IS the field name used in both providers.
|
|
128
|
+
//
|
|
129
|
+
// SendGrid: key is matched against custom field names at runtime (fetched + cached).
|
|
130
|
+
// Beehiiv: key is used directly as the custom field name.
|
|
131
|
+
//
|
|
132
|
+
// Source types:
|
|
133
|
+
// 'user' — read from user doc via _.get(userDoc, path)
|
|
134
|
+
// 'resolved' — read from User.resolveSubscription() output via path
|
|
135
|
+
// 'config' — read from Manager.config via _.get(config, path)
|
|
136
|
+
//
|
|
137
|
+
// To add a new tracked marketing field:
|
|
138
|
+
// 1. Add an entry here — the key becomes the field name in both providers
|
|
139
|
+
// 2. Add matching entry in OMEGA's src/lib/bem-fields.js (name, display, type)
|
|
140
|
+
// 3. Run OMEGA: npm start -- --service=sendgrid,beehiiv --brand=X
|
|
141
|
+
// 4. BEM resolves field IDs at runtime — no provider code changes needed
|
|
142
|
+
// 5. If 'resolved' source, ensure resolveFieldValues() computes it
|
|
143
|
+
const FIELDS = {
|
|
144
|
+
// Brand
|
|
145
|
+
brand_id: { source: 'config', path: 'brand.id', type: 'text' },
|
|
146
|
+
|
|
147
|
+
// User identity
|
|
148
|
+
user_auth_uid: { source: 'user', path: 'auth.uid', type: 'text' },
|
|
149
|
+
user_personal_country: { source: 'user', path: 'personal.location.country', type: 'text' },
|
|
150
|
+
user_metadata_signup_date: { source: 'user', path: 'metadata.created.timestamp', type: 'date' },
|
|
151
|
+
user_metadata_last_activity: { source: 'user', path: 'metadata.updated.timestamp', type: 'date' },
|
|
152
|
+
|
|
153
|
+
// Subscription
|
|
154
|
+
user_subscription_plan: { source: 'resolved', path: 'plan', type: 'text' },
|
|
155
|
+
user_subscription_status: { source: 'resolved', path: 'status', type: 'text' },
|
|
156
|
+
user_subscription_trialing: { source: 'resolved', path: 'trialing', type: 'text' },
|
|
157
|
+
user_subscription_cancelling: { source: 'resolved', path: 'cancelling', type: 'text' },
|
|
158
|
+
user_subscription_ever_paid: { source: 'resolved', path: 'everPaid', type: 'text' },
|
|
159
|
+
user_subscription_payment_processor: { source: 'user', path: 'subscription.payment.processor', type: 'text' },
|
|
160
|
+
user_subscription_payment_frequency: { source: 'user', path: 'subscription.payment.frequency', type: 'text' },
|
|
161
|
+
user_subscription_payment_price: { source: 'user', path: 'subscription.payment.price', type: 'number' },
|
|
162
|
+
user_subscription_payment_last_date: { source: 'user', path: 'subscription.payment.updatedBy.date.timestamp', type: 'date' },
|
|
163
|
+
|
|
164
|
+
// Attribution
|
|
165
|
+
user_attribution_utm_source: { source: 'user', path: 'attribution.utm.tags.utm_source', type: 'text' },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Resolve all field values from a user doc + config.
|
|
171
|
+
* Returns a map of semantic field names → resolved values (type-coerced).
|
|
172
|
+
* Providers use this internally to build their native field format.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} userDoc - User document from Firestore
|
|
175
|
+
* @param {object} config - Manager.config
|
|
176
|
+
* @returns {object} Map of semantic name → value (e.g., { plan: 'basic', status: 'active', ... })
|
|
177
|
+
*/
|
|
178
|
+
const _ = require('lodash');
|
|
179
|
+
const User = require('../../helpers/user.js');
|
|
180
|
+
|
|
181
|
+
function resolveFieldValues(userDoc, config) {
|
|
182
|
+
const resolved = User.resolveSubscription(userDoc);
|
|
183
|
+
const subscription = userDoc.subscription || {};
|
|
184
|
+
|
|
185
|
+
// Computed values from resolveSubscription() + raw status
|
|
186
|
+
const resolvedValues = {
|
|
187
|
+
plan: resolved.plan,
|
|
188
|
+
status: subscription.status || 'active',
|
|
189
|
+
everPaid: String(resolved.everPaid),
|
|
190
|
+
trialing: String(resolved.trialing),
|
|
191
|
+
cancelling: String(resolved.cancelling),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const result = {};
|
|
195
|
+
|
|
196
|
+
for (const [name, fieldConfig] of Object.entries(FIELDS)) {
|
|
197
|
+
let value;
|
|
198
|
+
|
|
199
|
+
if (fieldConfig.source === 'config') {
|
|
200
|
+
value = _.get(config, fieldConfig.path);
|
|
201
|
+
} else if (fieldConfig.source === 'resolved') {
|
|
202
|
+
value = resolvedValues[fieldConfig.path];
|
|
203
|
+
} else {
|
|
204
|
+
value = _.get(userDoc, fieldConfig.path);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (value == null) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Coerce booleans to strings for text fields
|
|
212
|
+
if (fieldConfig.type === 'text' && typeof value === 'boolean') {
|
|
213
|
+
value = String(value);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Coerce to number for number fields
|
|
217
|
+
if (fieldConfig.type === 'number' && typeof value !== 'number') {
|
|
218
|
+
value = Number(value) || 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Skip epoch default dates (1970-01-01)
|
|
222
|
+
if (fieldConfig.type === 'date' && (!value || value === '1970-01-01T00:00:00.000Z')) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
result[name] = value;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
TEMPLATES,
|
|
234
|
+
GROUPS,
|
|
235
|
+
SENDERS,
|
|
236
|
+
FIELDS,
|
|
237
|
+
DEFAULT_PROVIDERS,
|
|
238
|
+
SEND_AT_LIMIT,
|
|
239
|
+
sanitizeImagesForEmail,
|
|
240
|
+
encode,
|
|
241
|
+
errorWithCode,
|
|
242
|
+
resolveFieldValues,
|
|
243
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified email library — transactional + marketing
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const email = Manager.Email(assistant);
|
|
6
|
+
*
|
|
7
|
+
* // Transactional (default)
|
|
8
|
+
* await email.send({ to, subject, template, ... });
|
|
9
|
+
* await email.build({ ... });
|
|
10
|
+
*
|
|
11
|
+
* // Marketing campaign
|
|
12
|
+
* await email.send({ type: 'marketing', name, subject, segments, ... });
|
|
13
|
+
*
|
|
14
|
+
* // Add a new contact (newsletter subscribe)
|
|
15
|
+
* await email.add({ email, firstName, lastName, source });
|
|
16
|
+
*
|
|
17
|
+
* // Sync full user data to SendGrid/Beehiiv (all custom fields)
|
|
18
|
+
* await email.sync(userDoc);
|
|
19
|
+
*
|
|
20
|
+
* // Remove contact from all providers
|
|
21
|
+
* await email.remove('user@example.com');
|
|
22
|
+
*
|
|
23
|
+
* // Campaign management
|
|
24
|
+
* await email.cancelCampaign(id);
|
|
25
|
+
* await email.getCampaign(id);
|
|
26
|
+
* await email.listCampaigns({ status: 'scheduled' });
|
|
27
|
+
*/
|
|
28
|
+
const Transactional = require('./transactional/index.js');
|
|
29
|
+
const Marketing = require('./marketing/index.js');
|
|
30
|
+
|
|
31
|
+
function Email(assistant) {
|
|
32
|
+
const self = this;
|
|
33
|
+
|
|
34
|
+
self.assistant = assistant;
|
|
35
|
+
self.Manager = assistant.Manager;
|
|
36
|
+
|
|
37
|
+
// Compose internal modules
|
|
38
|
+
self._transactional = new Transactional(assistant);
|
|
39
|
+
self._marketing = new Marketing(assistant);
|
|
40
|
+
|
|
41
|
+
return self;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Send an email.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} settings
|
|
48
|
+
* @param {string} [settings.type] - 'transactional' (default) or 'marketing'
|
|
49
|
+
*
|
|
50
|
+
* Transactional settings: { to, subject, template, sender, sendAt, data, ... }
|
|
51
|
+
* Marketing settings: { name, subject, template, sender, segments, lists, sendAt, ... }
|
|
52
|
+
*
|
|
53
|
+
* @returns {object} Result from the appropriate sender
|
|
54
|
+
*/
|
|
55
|
+
Email.prototype.send = function (settings) {
|
|
56
|
+
const self = this;
|
|
57
|
+
const type = settings.type || 'transactional';
|
|
58
|
+
|
|
59
|
+
if (type === 'marketing') {
|
|
60
|
+
return self._marketing.sendCampaign(settings);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return self._transactional.send(settings);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build a transactional email without sending it.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} settings - Same as send() transactional settings
|
|
70
|
+
* @returns {object} SendGrid-ready email object
|
|
71
|
+
*/
|
|
72
|
+
Email.prototype.build = function (settings) {
|
|
73
|
+
return this._transactional.build(settings);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Sync a user's data to marketing providers (SendGrid + Beehiiv).
|
|
78
|
+
*
|
|
79
|
+
* @param {object} userDoc - Full user document from Firestore
|
|
80
|
+
* @param {object} [options] - { providers: array of provider names (default: DEFAULT_PROVIDERS) }
|
|
81
|
+
* @returns {{ sendgrid?: object, beehiiv?: object }}
|
|
82
|
+
*/
|
|
83
|
+
/**
|
|
84
|
+
* Add a new contact to marketing providers (lightweight, no full user doc needed).
|
|
85
|
+
*
|
|
86
|
+
* @param {object} options - { email, firstName, lastName, source, customFields, providers }
|
|
87
|
+
* @returns {{ sendgrid?: object, beehiiv?: object }}
|
|
88
|
+
*/
|
|
89
|
+
Email.prototype.add = function (options) {
|
|
90
|
+
return this._marketing.add(options);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Sync a user's full data to marketing providers (SendGrid + Beehiiv).
|
|
95
|
+
*
|
|
96
|
+
* @param {object} userDoc - Full user document from Firestore
|
|
97
|
+
* @param {object} [options] - { providers: array of provider names (default: DEFAULT_PROVIDERS) }
|
|
98
|
+
* @returns {{ sendgrid?: object, beehiiv?: object }}
|
|
99
|
+
*/
|
|
100
|
+
Email.prototype.sync = function (userDoc, options) {
|
|
101
|
+
return this._marketing.sync(userDoc, options);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Remove a contact from all marketing providers.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} email - Email address to remove
|
|
108
|
+
* @param {object} [options] - { providers: array of provider names (default: DEFAULT_PROVIDERS) }
|
|
109
|
+
* @returns {{ sendgrid?: object, beehiiv?: object }}
|
|
110
|
+
*/
|
|
111
|
+
Email.prototype.remove = function (email, options) {
|
|
112
|
+
return this._marketing.remove(email, options);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Cancel a scheduled marketing campaign.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} campaignId - Single Send ID
|
|
119
|
+
* @returns {{ success: boolean, error?: string }}
|
|
120
|
+
*/
|
|
121
|
+
Email.prototype.cancelCampaign = function (campaignId) {
|
|
122
|
+
return this._marketing.cancelCampaign(campaignId);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get a marketing campaign by ID.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} campaignId - Single Send ID
|
|
129
|
+
* @returns {object|null}
|
|
130
|
+
*/
|
|
131
|
+
Email.prototype.getCampaign = function (campaignId) {
|
|
132
|
+
return this._marketing.getCampaign(campaignId);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* List marketing campaigns.
|
|
137
|
+
*
|
|
138
|
+
* @param {object} [options] - { status: 'draft' | 'scheduled' | 'triggered' }
|
|
139
|
+
* @returns {Array<object>}
|
|
140
|
+
*/
|
|
141
|
+
Email.prototype.listCampaigns = function (options) {
|
|
142
|
+
return this._marketing.listCampaigns(options);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
module.exports = Email;
|