backend-manager 5.0.89 → 5.0.92
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 +2 -2
- package/CLAUDE.md +147 -8
- package/README.md +6 -6
- package/TODO-MARKETING.md +3 -0
- package/TODO-PAYMENT-v2.md +71 -0
- package/TODO.md +7 -0
- package/package.json +7 -5
- package/src/cli/commands/{emulators.js → emulator.js} +15 -15
- package/src/cli/commands/index.js +1 -1
- package/src/cli/commands/setup-tests/{emulators-config.js → emulator-config.js} +4 -4
- package/src/cli/commands/setup-tests/index.js +2 -2
- package/src/cli/commands/setup-tests/project-id-consistency.js +1 -1
- package/src/cli/commands/test.js +16 -16
- package/src/cli/index.js +15 -4
- package/src/manager/events/auth/on-create.js +5 -158
- package/src/manager/events/firestore/payments-webhooks/analytics.js +171 -0
- package/src/manager/events/firestore/payments-webhooks/on-write.js +95 -297
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +19 -10
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +4 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +61 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +22 -9
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +21 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +18 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +18 -7
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +26 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +24 -9
- package/src/manager/functions/core/actions/api/admin/send-email.js +0 -131
- package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +2 -137
- package/src/manager/functions/core/actions/api/user/sign-up.js +1 -1
- package/src/manager/helpers/user.js +1 -0
- package/src/manager/index.js +12 -0
- package/src/manager/libraries/email.js +483 -0
- package/src/manager/libraries/infer-contact.js +140 -0
- package/src/manager/libraries/payment-processors/resolve-price-id.js +19 -0
- package/src/manager/libraries/payment-processors/stripe.js +87 -48
- package/src/manager/libraries/payment-processors/test.js +4 -4
- package/src/manager/libraries/prompts/infer-contact.md +43 -0
- package/src/manager/routes/admin/backup/post.js +4 -3
- package/src/manager/routes/admin/email/post.js +11 -428
- package/src/manager/routes/admin/hook/post.js +3 -2
- package/src/manager/routes/admin/notification/post.js +14 -12
- package/src/manager/routes/admin/post/post.js +5 -6
- package/src/manager/routes/admin/post/put.js +3 -2
- package/src/manager/routes/admin/stats/get.js +19 -10
- package/src/manager/routes/general/email/post.js +8 -21
- package/src/manager/routes/marketing/contact/post.js +2 -100
- package/src/manager/routes/payments/intent/post.js +44 -2
- package/src/manager/routes/payments/intent/processors/stripe.js +10 -45
- package/src/manager/routes/payments/intent/processors/test.js +20 -25
- package/src/manager/routes/user/oauth2/_helpers.js +3 -2
- package/src/manager/routes/user/oauth2/delete.js +3 -3
- package/src/manager/routes/user/oauth2/get.js +2 -2
- package/src/manager/routes/user/oauth2/post.js +9 -9
- package/src/manager/routes/user/sessions/delete.js +4 -3
- package/src/manager/routes/user/signup/post.js +254 -54
- package/src/manager/schemas/admin/email/post.js +10 -5
- package/src/test/run-tests.js +1 -1
- package/src/test/runner.js +11 -0
- package/src/test/test-accounts.js +18 -0
- package/templates/backend-manager-config.json +31 -12
- package/test/events/payments/journey-payments-one-time-failure.js +105 -0
- package/test/events/payments/journey-payments-one-time.js +128 -0
- package/test/events/payments/journey-payments-plan-change.js +126 -0
- package/test/events/payments/journey-payments-upgrade.js +2 -2
- package/test/functions/admin/send-email.js +1 -88
- package/test/helpers/email.js +381 -0
- package/test/helpers/infer-contact.js +299 -0
- package/test/routes/admin/email.js +41 -90
- package/REFACTOR-BEM-API.md +0 -76
- package/REFACTOR-MIDDLEWARE.md +0 -62
- package/REFACTOR-PAYMENT.md +0 -66
- /package/bin/{bem → backend-manager} +0 -0
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Transition: payment-failed
|
|
3
3
|
* Triggered when a subscription payment fails (active → suspended)
|
|
4
|
-
*
|
|
5
|
-
* Use cases:
|
|
6
|
-
* - Send payment failure notification email
|
|
7
|
-
* - Include link to update payment method
|
|
8
|
-
* - Fire analytics event for churn risk
|
|
9
4
|
*/
|
|
10
|
-
|
|
5
|
+
const { sendOrderEmail, formatDate } = require('../send-email.js');
|
|
6
|
+
|
|
7
|
+
module.exports = async function ({ before, after, order, uid, assistant }) {
|
|
11
8
|
assistant.log(`Transition [subscription/payment-failed]: uid=${uid}, product=${after.product.id}, previousStatus=${before?.status}`);
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
sendOrderEmail({
|
|
11
|
+
template: 'd-e56af0ac62364bfb9e50af02854e2cd3',
|
|
12
|
+
subject: 'Your payment failed',
|
|
13
|
+
categories: ['order/payment-failed'],
|
|
14
|
+
uid,
|
|
15
|
+
assistant,
|
|
16
|
+
data: {
|
|
17
|
+
order: {
|
|
18
|
+
...order,
|
|
19
|
+
_computed: {
|
|
20
|
+
date: formatDate(new Date().toISOString()),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
15
25
|
};
|
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Transition: payment-recovered
|
|
3
3
|
* Triggered when a suspended subscription is recovered (suspended → active)
|
|
4
|
-
*
|
|
5
|
-
* Use cases:
|
|
6
|
-
* - Send payment recovered confirmation email
|
|
7
|
-
* - Fire analytics event for recovered subscriber
|
|
8
4
|
*/
|
|
9
|
-
|
|
5
|
+
const { sendOrderEmail, formatDate } = require('../send-email.js');
|
|
6
|
+
|
|
7
|
+
module.exports = async function ({ before, after, order, uid, assistant }) {
|
|
10
8
|
assistant.log(`Transition [subscription/payment-recovered]: uid=${uid}, product=${after.product.id}`);
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
sendOrderEmail({
|
|
11
|
+
template: 'd-d6dbd17a260a4755b34a852ba09c2454',
|
|
12
|
+
subject: 'Your payment was successful',
|
|
13
|
+
categories: ['order/payment-recovered'],
|
|
14
|
+
uid,
|
|
15
|
+
assistant,
|
|
16
|
+
data: {
|
|
17
|
+
order: {
|
|
18
|
+
...order,
|
|
19
|
+
_computed: {
|
|
20
|
+
date: formatDate(new Date().toISOString()),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
14
25
|
};
|
package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Transition: plan-changed
|
|
3
3
|
* Triggered when a user upgrades or downgrades their plan (product A → product B, both active + paid)
|
|
4
|
-
*
|
|
5
|
-
* Use cases:
|
|
6
|
-
* - Send plan change confirmation email
|
|
7
|
-
* - Include new plan details and what changed
|
|
8
|
-
* - Fire analytics event for upgrade/downgrade
|
|
9
4
|
*/
|
|
10
|
-
|
|
5
|
+
const { sendOrderEmail, formatDate } = require('../send-email.js');
|
|
6
|
+
|
|
7
|
+
module.exports = async function ({ before, after, order, uid, assistant }) {
|
|
11
8
|
const direction = after.product.id > before.product.id ? 'upgrade' : 'downgrade';
|
|
12
9
|
assistant.log(`Transition [subscription/plan-changed]: uid=${uid}, ${before.product.id} → ${after.product.id} (${direction})`);
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
sendOrderEmail({
|
|
12
|
+
template: 'd-399086311bbb48b4b77bc90b20fb9d0a',
|
|
13
|
+
subject: 'Your subscription plan has been updated',
|
|
14
|
+
categories: ['order/plan-changed'],
|
|
15
|
+
uid,
|
|
16
|
+
assistant,
|
|
17
|
+
data: {
|
|
18
|
+
order: {
|
|
19
|
+
...order,
|
|
20
|
+
// Inject previous plan info into the unified object for the template
|
|
21
|
+
unified: {
|
|
22
|
+
...order.unified,
|
|
23
|
+
previous: {
|
|
24
|
+
product: before.product,
|
|
25
|
+
price: before.payment?.price || 0,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
_computed: {
|
|
29
|
+
date: formatDate(new Date().toISOString()),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
16
34
|
};
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Transition: subscription-cancelled
|
|
3
3
|
* Triggered when a subscription is fully cancelled (any non-cancelled → cancelled)
|
|
4
|
-
*
|
|
5
|
-
* Use cases:
|
|
6
|
-
* - Send final cancellation email
|
|
7
|
-
* - Include reactivation link or win-back offer
|
|
8
|
-
* - Fire analytics event for churned subscriber
|
|
9
|
-
* - Clean up any subscription-specific resources
|
|
10
4
|
*/
|
|
11
|
-
|
|
5
|
+
const { sendOrderEmail, formatDate } = require('../send-email.js');
|
|
6
|
+
|
|
7
|
+
module.exports = async function ({ before, after, order, uid, assistant }) {
|
|
12
8
|
assistant.log(`Transition [subscription/subscription-cancelled]: uid=${uid}, previousProduct=${before?.product?.id}, previousStatus=${before?.status}`);
|
|
13
9
|
|
|
14
|
-
//
|
|
15
|
-
|
|
10
|
+
// Check if subscription has a future expiry (e.g., cancelled at period end)
|
|
11
|
+
const hasFutureExpiry = after.expires?.timestamp && new Date(after.expires.timestamp) > new Date();
|
|
12
|
+
|
|
13
|
+
sendOrderEmail({
|
|
14
|
+
template: 'd-39041132e6b24e5ebf0e95bce2d94dba',
|
|
15
|
+
subject: 'Your subscription has been cancelled',
|
|
16
|
+
categories: ['order/cancelled'],
|
|
17
|
+
uid,
|
|
18
|
+
assistant,
|
|
19
|
+
data: {
|
|
20
|
+
order: {
|
|
21
|
+
...order,
|
|
22
|
+
_computed: {
|
|
23
|
+
date: formatDate(new Date().toISOString()),
|
|
24
|
+
...(hasFutureExpiry && {
|
|
25
|
+
expiresDate: formatDate(after.expires.timestamp),
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
16
31
|
};
|
|
@@ -51,18 +51,6 @@ Module.prototype.main = function () {
|
|
|
51
51
|
return reject(assistant.errorify(email.message, { code: 400 }));
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// Check for duplicate emails being sent
|
|
55
|
-
const uniqueResult = await self.ensureFirstInstance(email);
|
|
56
|
-
|
|
57
|
-
// If not unique, return early
|
|
58
|
-
if (!uniqueResult) {
|
|
59
|
-
return resolve({
|
|
60
|
-
data: {
|
|
61
|
-
status: 'non-unique',
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
54
|
// If scheduled beyond SendGrid's limit, queue it
|
|
67
55
|
if (email.sendAt && email.sendAt >= moment().add(SEND_AT_LIMIT, 'hours').unix()) {
|
|
68
56
|
await self.saveToEmailQueue(email).catch(e => e);
|
|
@@ -151,7 +139,6 @@ Module.prototype.defaultize = function () {
|
|
|
151
139
|
|
|
152
140
|
// Set defaults
|
|
153
141
|
options.copy = typeof options.copy === 'undefined' ? true : options.copy;
|
|
154
|
-
options.ensureUnique = typeof options.ensureUnique === 'undefined' ? true : options.ensureUnique;
|
|
155
142
|
options.categories = powertools.arrayify(options.categories || []);
|
|
156
143
|
|
|
157
144
|
email.to = powertools.arrayify(options.to || []);
|
|
@@ -262,38 +249,6 @@ Module.prototype.defaultize = function () {
|
|
|
262
249
|
email.cc = email.cc.filter(obj => !email.to.some(obj2 => obj.email === obj2.email));
|
|
263
250
|
email.bcc = email.bcc.filter(obj => !email.to.some(obj2 => obj.email === obj2.email));
|
|
264
251
|
|
|
265
|
-
// Try to get contact name from SendGrid
|
|
266
|
-
await fetch(`https://api.sendgrid.com/v3/marketing/contacts/search/emails`, {
|
|
267
|
-
method: 'post',
|
|
268
|
-
response: 'json',
|
|
269
|
-
timeout: 60000,
|
|
270
|
-
headers: {
|
|
271
|
-
'Authorization': `Bearer ${process.env.SENDGRID_API_KEY}`,
|
|
272
|
-
'Content-Type': 'application/json',
|
|
273
|
-
},
|
|
274
|
-
body: {
|
|
275
|
-
emails: email.to.map(obj => obj.email),
|
|
276
|
-
},
|
|
277
|
-
})
|
|
278
|
-
.then((json) => {
|
|
279
|
-
assistant.log('Got contact names', json);
|
|
280
|
-
|
|
281
|
-
// Update names from contacts
|
|
282
|
-
email.to.forEach((to) => {
|
|
283
|
-
const match = json.result[to.email];
|
|
284
|
-
if (match) {
|
|
285
|
-
email.to[0].name = match.contact.first_name || email.dynamicTemplateData.user.personal.name.first;
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
})
|
|
289
|
-
.catch((e) => {
|
|
290
|
-
if (e.status === 404) {
|
|
291
|
-
assistant.log('Contact does not exist in database');
|
|
292
|
-
} else {
|
|
293
|
-
assistant.error('Failed to get contact names', e);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
|
|
297
252
|
// Log resolved email
|
|
298
253
|
assistant.log('Resolved email.to', email.to);
|
|
299
254
|
|
|
@@ -331,7 +286,6 @@ Module.prototype.defaultize = function () {
|
|
|
331
286
|
email.dynamicTemplateData.email.unsubscribeUrl = `https://itwcreativeworks.com/portal/account/email-preferences?email=${encode(email.to[0].email)}&asmId=${encode(email.asm.groupId)}&templateId=${encode(email.templateId)}&appName=${email.dynamicTemplateData.app.name}&appUrl=${email.dynamicTemplateData.app.url}`;
|
|
332
287
|
email.dynamicTemplateData.email.categories = email.categories;
|
|
333
288
|
email.dynamicTemplateData.email.carbonCopy = options.copy;
|
|
334
|
-
email.dynamicTemplateData.email.ensureUnique = options.ensureUnique;
|
|
335
289
|
|
|
336
290
|
// Handle raw HTML content (overrides template)
|
|
337
291
|
if (options.html) {
|
|
@@ -350,16 +304,6 @@ Module.prototype.defaultize = function () {
|
|
|
350
304
|
'List-Unsubscribe': `<${email.dynamicTemplateData.email.unsubscribeUrl}>`,
|
|
351
305
|
};
|
|
352
306
|
|
|
353
|
-
// Generate email hash for deduplication
|
|
354
|
-
email.hash = crypto.createHash('sha256');
|
|
355
|
-
email.hash.update(
|
|
356
|
-
email.to.map(obj => obj.email).join(',')
|
|
357
|
-
+ email.from.email
|
|
358
|
-
+ email.subject
|
|
359
|
-
+ options.categories.join(',')
|
|
360
|
-
);
|
|
361
|
-
email.hash = email.hash.digest('hex');
|
|
362
|
-
|
|
363
307
|
// Clone and clean data for stringified version
|
|
364
308
|
const emailClonedData = _.cloneDeep(email.dynamicTemplateData);
|
|
365
309
|
emailClonedData.app.sponsorships = {};
|
|
@@ -400,81 +344,6 @@ Module.prototype.saveToEmailQueue = function (email) {
|
|
|
400
344
|
});
|
|
401
345
|
};
|
|
402
346
|
|
|
403
|
-
Module.prototype.ensureFirstInstance = function (email) {
|
|
404
|
-
const self = this;
|
|
405
|
-
const Manager = self.Manager;
|
|
406
|
-
const assistant = self.assistant;
|
|
407
|
-
const payload = self.payload;
|
|
408
|
-
|
|
409
|
-
return new Promise(async function(resolve, reject) {
|
|
410
|
-
const timeout = assistant.isDevelopment() ? 3000 : 45000;
|
|
411
|
-
const { admin } = self.libraries;
|
|
412
|
-
|
|
413
|
-
const hash = email.hash;
|
|
414
|
-
const id = email.dynamicTemplateData.email.id;
|
|
415
|
-
const options = payload.data.payload;
|
|
416
|
-
|
|
417
|
-
assistant.log(`ensureFirstInstance(): Checking for unique email hash=${hash}, id=${id}`);
|
|
418
|
-
|
|
419
|
-
// Skip uniqueness check if disabled
|
|
420
|
-
if (!options.ensureUnique) {
|
|
421
|
-
assistant.log(`ensureFirstInstance(): Skipping unique email check`);
|
|
422
|
-
return resolve(true);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Save email to temporary storage
|
|
426
|
-
await admin.firestore().doc(`temporary/email-queue`).set({
|
|
427
|
-
[hash]: {
|
|
428
|
-
[id]: assistant.meta.startTime.timestampUNIX,
|
|
429
|
-
},
|
|
430
|
-
}, { merge: true })
|
|
431
|
-
.then((doc) => {
|
|
432
|
-
assistant.log(`ensureFirstInstance(): Saved email to temporary storage`, hash);
|
|
433
|
-
})
|
|
434
|
-
.catch((e) => {
|
|
435
|
-
assistant.error(`ensureFirstInstance(): Failed to save email to temporary storage`, hash, e);
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// Wait for timeout to allow duplicates to register
|
|
439
|
-
assistant.log(`ensureFirstInstance(): Waiting for ${timeout / 1000} sec`);
|
|
440
|
-
await powertools.poll(async (index) => {
|
|
441
|
-
return false;
|
|
442
|
-
}, { interval: 1000, timeout: timeout })
|
|
443
|
-
.catch((e) => {
|
|
444
|
-
assistant.log(`ensureFirstInstance(): Timeout reached`);
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
// Check if this is the first instance
|
|
448
|
-
const result = await admin.firestore().doc(`temporary/email-queue`).get()
|
|
449
|
-
.then((doc) => doc.data()?.[hash] || {})
|
|
450
|
-
.catch((e) => ({}));
|
|
451
|
-
|
|
452
|
-
const length = Object.keys(result).length;
|
|
453
|
-
const isFirstInstance = length === 1 || result[id] === Math.min(...Object.values(result));
|
|
454
|
-
|
|
455
|
-
assistant.log(`ensureFirstInstance(): Result`, result);
|
|
456
|
-
assistant.log(`ensureFirstInstance(): Result isFirstInstance`, length, isFirstInstance);
|
|
457
|
-
|
|
458
|
-
if (isFirstInstance) {
|
|
459
|
-
// Delete email from temporary storage
|
|
460
|
-
await admin.firestore().doc(`temporary/email-queue`).set({
|
|
461
|
-
[hash]: FieldValue.delete(),
|
|
462
|
-
}, { merge: true })
|
|
463
|
-
.then((doc) => {
|
|
464
|
-
assistant.log(`ensureFirstInstance(): Deleted email from temporary storage`, hash);
|
|
465
|
-
})
|
|
466
|
-
.catch((e) => {
|
|
467
|
-
assistant.error(`ensureFirstInstance(): Failed to delete email from temporary storage`, hash, e);
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
return resolve(true);
|
|
471
|
-
} else {
|
|
472
|
-
assistant.warn(`ensureFirstInstance(): Email is not unique`, hash, length, result);
|
|
473
|
-
return resolve(false);
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
};
|
|
477
|
-
|
|
478
347
|
// Helper to URL-encode base64
|
|
479
348
|
function encode(s) {
|
|
480
349
|
return encodeURIComponent(Buffer.from(String(s)).toString('base64'));
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
const fetch = require('wonderful-fetch');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const dns = require('dns').promises;
|
|
4
|
-
const OpenAI = require(path.join(__dirname, '..', '..', '..', '..', '..', 'libraries', 'openai'));
|
|
5
4
|
|
|
6
5
|
// Load disposable domains list
|
|
7
6
|
const DISPOSABLE_DOMAINS = require(path.join(__dirname, '..', '..', '..', '..', '..', 'libraries', 'disposable-domains.json'));
|
|
8
7
|
const DISPOSABLE_SET = new Set(DISPOSABLE_DOMAINS.map(d => d.toLowerCase()));
|
|
8
|
+
const { inferContact } = require(path.join(__dirname, '..', '..', '..', '..', '..', 'libraries', 'infer-contact.js'));
|
|
9
9
|
|
|
10
10
|
function Module() {}
|
|
11
11
|
|
|
@@ -108,7 +108,7 @@ Module.prototype.main = function () {
|
|
|
108
108
|
// Infer name if not provided
|
|
109
109
|
let nameInferred = null;
|
|
110
110
|
if (!firstName && !lastName) {
|
|
111
|
-
nameInferred = await
|
|
111
|
+
nameInferred = await inferContact(email, assistant);
|
|
112
112
|
firstName = nameInferred.firstName;
|
|
113
113
|
lastName = nameInferred.lastName;
|
|
114
114
|
}
|
|
@@ -248,141 +248,6 @@ async function checkMxRecord(domain) {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
/**
|
|
252
|
-
* Infer first/last name from email address
|
|
253
|
-
* Uses ChatGPT if OPENAI_API_KEY exists, otherwise regex parsing
|
|
254
|
-
*/
|
|
255
|
-
async function inferName(email, assistant) {
|
|
256
|
-
// Try ChatGPT first if available
|
|
257
|
-
if (process.env.OPENAI_API_KEY) {
|
|
258
|
-
const aiResult = await inferNameWithAI(email, assistant);
|
|
259
|
-
if (aiResult && (aiResult.firstName || aiResult.lastName)) {
|
|
260
|
-
return aiResult;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Fallback to regex parsing
|
|
265
|
-
return inferNameFromEmail(email);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Use ChatGPT to infer name from email
|
|
270
|
-
*/
|
|
271
|
-
async function inferNameWithAI(email, assistant) {
|
|
272
|
-
try {
|
|
273
|
-
const ai = new OpenAI(assistant);
|
|
274
|
-
const result = await ai.request({
|
|
275
|
-
// model: 'gpt-4.1-nano',
|
|
276
|
-
model: 'gpt-5-mini',
|
|
277
|
-
timeout: 30000,
|
|
278
|
-
maxTokens: 1024,
|
|
279
|
-
moderate: false,
|
|
280
|
-
response: 'json',
|
|
281
|
-
prompt: {
|
|
282
|
-
content: `
|
|
283
|
-
<identity>
|
|
284
|
-
You extract names and company from email addresses.
|
|
285
|
-
</identity>
|
|
286
|
-
|
|
287
|
-
<format>
|
|
288
|
-
Return ONLY valid JSON like so:
|
|
289
|
-
{
|
|
290
|
-
"firstName": "...", // First name <string>, capitalized
|
|
291
|
-
"lastName": "...", // Last name <string>, capitalized
|
|
292
|
-
"company": "...", // Company name <string>, capitalized
|
|
293
|
-
"confidence": "..." // Confidence level <number>, 0-1 scale
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
If you cannot determine a name, use empty strings.
|
|
297
|
-
</format>
|
|
298
|
-
|
|
299
|
-
<examples>
|
|
300
|
-
<example>
|
|
301
|
-
<input>john.smith@acme.com</input>
|
|
302
|
-
<output>{"firstName": "John", "lastName": "Smith", "company": "Acme", "confidence": 0.9}</output>
|
|
303
|
-
</example>
|
|
304
|
-
<example>
|
|
305
|
-
<input>jsmith123@gmail.com</input>
|
|
306
|
-
<output>{"firstName": "J", "lastName": "Smith", "company": "", "confidence": 0.4}</output>
|
|
307
|
-
</example>
|
|
308
|
-
<example>
|
|
309
|
-
<input>support@bigcorp.io</input>
|
|
310
|
-
<output>{"firstName": "", "lastName": "", "company": "Bigcorp", "confidence": 0.7}</output>
|
|
311
|
-
</example>
|
|
312
|
-
<example>
|
|
313
|
-
<input>mary_jane_watson@stark-industries.com</input>
|
|
314
|
-
<output>{"firstName": "Mary", "lastName": "Watson", "company": "Stark Industries", "confidence": 0.85}</output>
|
|
315
|
-
</example>
|
|
316
|
-
<example>
|
|
317
|
-
<input>info@company.org</input>
|
|
318
|
-
<output>{"firstName": "", "lastName": "", "company": "Company", "confidence": 0.6}</output>
|
|
319
|
-
</example>
|
|
320
|
-
</examples>
|
|
321
|
-
`,
|
|
322
|
-
},
|
|
323
|
-
message: {
|
|
324
|
-
content: `Email: ${email}`,
|
|
325
|
-
},
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
if (result?.firstName !== undefined) {
|
|
329
|
-
return {
|
|
330
|
-
firstName: capitalize(result.firstName || ''),
|
|
331
|
-
lastName: capitalize(result.lastName || ''),
|
|
332
|
-
company: capitalize(result.company || ''),
|
|
333
|
-
confidence: typeof result.confidence === 'number' ? result.confidence : 0.5,
|
|
334
|
-
method: 'ai',
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
} catch (e) {
|
|
338
|
-
console.error('AI name inference error:', e);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Regex-based name inference from email
|
|
346
|
-
*/
|
|
347
|
-
function inferNameFromEmail(email) {
|
|
348
|
-
const local = email.split('@')[0];
|
|
349
|
-
|
|
350
|
-
// Remove trailing numbers
|
|
351
|
-
const cleaned = local.replace(/[0-9]+$/, '');
|
|
352
|
-
|
|
353
|
-
// Split on common separators
|
|
354
|
-
const parts = cleaned.split(/[._-]/);
|
|
355
|
-
|
|
356
|
-
if (parts.length >= 2) {
|
|
357
|
-
return {
|
|
358
|
-
firstName: capitalize(parts[0]),
|
|
359
|
-
lastName: capitalize(parts.slice(1).join(' ')),
|
|
360
|
-
confidence: 0.5,
|
|
361
|
-
method: 'regex',
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return {
|
|
366
|
-
firstName: capitalize(cleaned),
|
|
367
|
-
lastName: '',
|
|
368
|
-
confidence: 0.25,
|
|
369
|
-
method: 'regex',
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Capitalize first letter of each word
|
|
375
|
-
*/
|
|
376
|
-
function capitalize(str) {
|
|
377
|
-
if (!str) {
|
|
378
|
-
return '';
|
|
379
|
-
}
|
|
380
|
-
return str
|
|
381
|
-
.split(' ')
|
|
382
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
383
|
-
.join(' ');
|
|
384
|
-
}
|
|
385
|
-
|
|
386
251
|
/**
|
|
387
252
|
* Add contact to SendGrid Marketing Contacts
|
|
388
253
|
*/
|
|
@@ -112,7 +112,7 @@ Module.prototype.main = function () {
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
// Note: SendGrid list
|
|
115
|
+
// Note: SendGrid list, welcome emails, and name inference are handled by user/signup route
|
|
116
116
|
|
|
117
117
|
return resolve({
|
|
118
118
|
data: {
|
|
@@ -41,6 +41,7 @@ const SCHEMA = {
|
|
|
41
41
|
orderId: { type: 'string', default: null, nullable: true },
|
|
42
42
|
resourceId: { type: 'string', default: null, nullable: true },
|
|
43
43
|
frequency: { type: 'string', default: null, nullable: true },
|
|
44
|
+
price: { type: 'number', default: 0 },
|
|
44
45
|
startDate: '$timestamp',
|
|
45
46
|
updatedBy: {
|
|
46
47
|
event: {
|
package/src/manager/index.js
CHANGED
|
@@ -507,6 +507,18 @@ Manager.prototype.Metadata = function () {
|
|
|
507
507
|
return new self.libraries.Metadata(self, ...arguments);
|
|
508
508
|
};
|
|
509
509
|
|
|
510
|
+
Manager.prototype.Email = function (assistant) {
|
|
511
|
+
const self = this;
|
|
512
|
+
self.libraries.Email = self.libraries.Email || require('./libraries/email.js');
|
|
513
|
+
return new self.libraries.Email(assistant);
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
Manager.prototype.AI = function (assistant, key) {
|
|
517
|
+
const self = this;
|
|
518
|
+
self.libraries.AI = self.libraries.AI || require('./libraries/openai.js');
|
|
519
|
+
return new self.libraries.AI(assistant, key);
|
|
520
|
+
};
|
|
521
|
+
|
|
510
522
|
// For importing API libraries
|
|
511
523
|
Manager.prototype.Api = function () {
|
|
512
524
|
const self = this;
|