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.
Files changed (72) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/CLAUDE.md +147 -8
  3. package/README.md +6 -6
  4. package/TODO-MARKETING.md +3 -0
  5. package/TODO-PAYMENT-v2.md +71 -0
  6. package/TODO.md +7 -0
  7. package/package.json +7 -5
  8. package/src/cli/commands/{emulators.js → emulator.js} +15 -15
  9. package/src/cli/commands/index.js +1 -1
  10. package/src/cli/commands/setup-tests/{emulators-config.js → emulator-config.js} +4 -4
  11. package/src/cli/commands/setup-tests/index.js +2 -2
  12. package/src/cli/commands/setup-tests/project-id-consistency.js +1 -1
  13. package/src/cli/commands/test.js +16 -16
  14. package/src/cli/index.js +15 -4
  15. package/src/manager/events/auth/on-create.js +5 -158
  16. package/src/manager/events/firestore/payments-webhooks/analytics.js +171 -0
  17. package/src/manager/events/firestore/payments-webhooks/on-write.js +95 -297
  18. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +19 -10
  19. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +4 -8
  20. package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +61 -0
  21. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +22 -9
  22. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +21 -8
  23. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +18 -8
  24. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +18 -7
  25. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +26 -8
  26. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +24 -9
  27. package/src/manager/functions/core/actions/api/admin/send-email.js +0 -131
  28. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +2 -137
  29. package/src/manager/functions/core/actions/api/user/sign-up.js +1 -1
  30. package/src/manager/helpers/user.js +1 -0
  31. package/src/manager/index.js +12 -0
  32. package/src/manager/libraries/email.js +483 -0
  33. package/src/manager/libraries/infer-contact.js +140 -0
  34. package/src/manager/libraries/payment-processors/resolve-price-id.js +19 -0
  35. package/src/manager/libraries/payment-processors/stripe.js +87 -48
  36. package/src/manager/libraries/payment-processors/test.js +4 -4
  37. package/src/manager/libraries/prompts/infer-contact.md +43 -0
  38. package/src/manager/routes/admin/backup/post.js +4 -3
  39. package/src/manager/routes/admin/email/post.js +11 -428
  40. package/src/manager/routes/admin/hook/post.js +3 -2
  41. package/src/manager/routes/admin/notification/post.js +14 -12
  42. package/src/manager/routes/admin/post/post.js +5 -6
  43. package/src/manager/routes/admin/post/put.js +3 -2
  44. package/src/manager/routes/admin/stats/get.js +19 -10
  45. package/src/manager/routes/general/email/post.js +8 -21
  46. package/src/manager/routes/marketing/contact/post.js +2 -100
  47. package/src/manager/routes/payments/intent/post.js +44 -2
  48. package/src/manager/routes/payments/intent/processors/stripe.js +10 -45
  49. package/src/manager/routes/payments/intent/processors/test.js +20 -25
  50. package/src/manager/routes/user/oauth2/_helpers.js +3 -2
  51. package/src/manager/routes/user/oauth2/delete.js +3 -3
  52. package/src/manager/routes/user/oauth2/get.js +2 -2
  53. package/src/manager/routes/user/oauth2/post.js +9 -9
  54. package/src/manager/routes/user/sessions/delete.js +4 -3
  55. package/src/manager/routes/user/signup/post.js +254 -54
  56. package/src/manager/schemas/admin/email/post.js +10 -5
  57. package/src/test/run-tests.js +1 -1
  58. package/src/test/runner.js +11 -0
  59. package/src/test/test-accounts.js +18 -0
  60. package/templates/backend-manager-config.json +31 -12
  61. package/test/events/payments/journey-payments-one-time-failure.js +105 -0
  62. package/test/events/payments/journey-payments-one-time.js +128 -0
  63. package/test/events/payments/journey-payments-plan-change.js +126 -0
  64. package/test/events/payments/journey-payments-upgrade.js +2 -2
  65. package/test/functions/admin/send-email.js +1 -88
  66. package/test/helpers/email.js +381 -0
  67. package/test/helpers/infer-contact.js +299 -0
  68. package/test/routes/admin/email.js +41 -90
  69. package/REFACTOR-BEM-API.md +0 -76
  70. package/REFACTOR-MIDDLEWARE.md +0 -62
  71. package/REFACTOR-PAYMENT.md +0 -66
  72. /package/bin/{bem → backend-manager} +0 -0
@@ -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
- module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
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
- // TODO: Send payment failure email with update-payment link
14
- // TODO: Fire analytics event
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
  };
@@ -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
- module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
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
- // TODO: Send payment recovered email
13
- // TODO: Fire analytics event
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
  };
@@ -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
- module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
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
- // TODO: Send plan change email with new plan details
15
- // TODO: Fire analytics event
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
- module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
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
- // TODO: Send cancellation email with reactivation link
15
- // TODO: Fire analytics event
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 inferName(email, assistant);
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 and welcome emails are now handled by auth:on-create
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: {
@@ -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;