backend-manager 5.0.121 → 5.0.123

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 (39) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/cli/commands/deploy.js +1 -1
  5. package/src/manager/cron/daily.js +2 -53
  6. package/src/manager/cron/frequent/abandoned-carts.js +148 -0
  7. package/src/manager/cron/frequent.js +3 -0
  8. package/src/manager/cron/runner.js +60 -0
  9. package/src/manager/events/firestore/payments-disputes/on-write.js +358 -0
  10. package/src/manager/events/firestore/payments-webhooks/analytics.js +245 -121
  11. package/src/manager/events/firestore/payments-webhooks/on-write.js +32 -2
  12. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +11 -34
  13. package/src/manager/helpers/analytics.js +2 -2
  14. package/src/manager/index.js +10 -0
  15. package/src/manager/libraries/abandoned-cart-config.js +12 -0
  16. package/src/manager/libraries/email.js +5 -5
  17. package/src/manager/libraries/openai.js +76 -7
  18. package/src/manager/libraries/payment/discount-codes.js +40 -0
  19. package/src/manager/libraries/recaptcha.js +36 -0
  20. package/src/manager/routes/app/get.js +1 -1
  21. package/src/manager/routes/marketing/contact/post.js +11 -29
  22. package/src/manager/routes/payments/discount/get.js +22 -0
  23. package/src/manager/routes/payments/dispute-alert/post.js +93 -0
  24. package/src/manager/routes/payments/dispute-alert/processors/chargeblast.js +43 -0
  25. package/src/manager/routes/payments/intent/post.js +29 -0
  26. package/src/manager/routes/payments/intent/processors/chargebee.js +59 -7
  27. package/src/manager/routes/payments/intent/processors/stripe.js +55 -7
  28. package/src/manager/schemas/payments/discount/get.js +9 -0
  29. package/src/manager/schemas/payments/dispute-alert/post.js +6 -0
  30. package/src/manager/schemas/payments/intent/post.js +16 -0
  31. package/src/test/runner.js +14 -4
  32. package/src/test/test-accounts.js +18 -0
  33. package/templates/backend-manager-config.json +7 -1
  34. package/templates/firestore.rules +9 -1
  35. package/test/routes/marketing/contact.js +3 -2
  36. package/test/routes/payments/discount.js +80 -0
  37. package/test/routes/payments/dispute-alert.js +271 -0
  38. package/test/routes/payments/intent.js +60 -0
  39. package/test/rules/payments-carts.js +371 -0
package/CHANGELOG.md CHANGED
@@ -14,6 +14,54 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
+ # [5.0.123] - 2026-03-10
18
+ ### Added
19
+ - Dispute alert system: `POST /payments/dispute-alert` endpoint with Chargeblast processor for ingesting payment dispute webhooks
20
+ - Firestore trigger (`payments-disputes/{alertId}`) that matches disputes to Stripe invoices by date/amount/card, auto-refunds, and cancels subscriptions
21
+ - Discount code system: `GET /payments/discount` validation endpoint and `discount-codes.js` library (FLASH20, SAVE10, WELCOME15)
22
+ - Discount code integration in payment intent flow — auto-creates/reuses Stripe and Chargebee coupons with deterministic IDs
23
+ - Meta Conversions API and TikTok Events API tracking alongside existing GA4 in payment analytics
24
+ - Subscription renewal tracking as payment events (fires on `invoice.payment_succeeded` / `PAYMENT.SALE.COMPLETED` even without a state transition)
25
+ - `attribution`, `discount`, and `supplemental` fields on payment intent schema for checkout context tracking
26
+ - Intent data (attribution, discount, supplemental) propagated to order objects during webhook on-write
27
+ - `meta.pixelId` and `tiktok.pixelCode` fields in config template
28
+ - Journey test accounts for discount and attribution flows
29
+ - Tests for discount validation and dispute alert endpoints
30
+
31
+ ### Changed
32
+ - Renamed config key `google_analytics` → `googleAnalytics`
33
+ - Payment analytics rewritten with independent per-platform fire functions (`fireGA4`, `fireMeta`, `fireTikTok`)
34
+ - Test runner module resolution now tries normal resolution first before falling back to search paths
35
+ - reCAPTCHA marketing contact test skipped when `TEST_EXTENDED_MODE` is not set
36
+
37
+ # [5.0.122] - 2026-03-09
38
+ ### Added
39
+ - Abandoned cart reminder system: sends escalating emails at 15min, 3h, 24h, 48h, 72h to users who visit checkout but don't complete payment
40
+ - `payments-carts/{uid}` Firestore collection with security rules (client-side write, server-side completion)
41
+ - `bm_cronFrequent` Cloud Function running every 10 minutes for sub-daily cron jobs
42
+ - Shared cron runner (`cron/runner.js`) consolidating daily and frequent cron orchestrators
43
+ - `main/order/refunded` and `main/order/abandoned-cart` email templates
44
+ - Firestore rules test for `payments-carts` (12 test cases)
45
+
46
+ ### Changed
47
+ - Migrated v1 email templates to v2 SendGrid template IDs
48
+ - `cron/daily.js` and `cron/frequent.js` now delegate to shared `cron/runner.js`
49
+ - Payment analytics tracking now fires independently of transitions
50
+
51
+ # [5.0.120] - 2026-03-09
52
+ ### Added
53
+ - reCAPTCHA verification on `POST /payments/intent` route (reads `verification.g-recaptcha-response` from request body)
54
+ - Shared `libraries/recaptcha.js` module for reCAPTCHA token verification (replaces duplicate helpers)
55
+ - `verification` field in `payments/intent` schema to accept the reCAPTCHA token object
56
+
57
+ ### Security
58
+ - reCAPTCHA failure responses now return generic "Request could not be verified" (403) instead of revealing the verification mechanism
59
+ - reCAPTCHA verification runs in all environments except automated tests (`isTesting()`)
60
+
61
+ ### Changed
62
+ - Marketing contact routes (`POST /marketing/contact`, `bm_api add-marketing-contact`) now use shared `recaptcha.verify()` instead of inline helpers
63
+ - Marketing reCAPTCHA checks skip during automated tests (consistent with payment intent)
64
+
17
65
  # [5.0.119] - 2026-03-07
18
66
  ### Added
19
67
  - `POST /marketing/email-preferences` route for unsubscribe/resubscribe via SendGrid ASM suppression groups
package/README.md CHANGED
@@ -139,7 +139,7 @@ Create `backend-manager-config.json` in your functions directory:
139
139
  sentry: {
140
140
  dsn: 'https://xxx@xxx.ingest.sentry.io/xxx',
141
141
  },
142
- google_analytics: {
142
+ googleAnalytics: {
143
143
  id: 'G-XXXXXXXXXX',
144
144
  secret: 'your-ga4-secret',
145
145
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.121",
3
+ "version": "5.0.123",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -13,7 +13,7 @@ class DeployCommand extends BaseCommand {
13
13
  }
14
14
 
15
15
  // Execute
16
- await powertools.execute('firebase deploy', { log: true });
16
+ await powertools.execute('firebase deploy', { log: true, config: { cwd: self.firebaseProjectPath } });
17
17
  }
18
18
  }
19
19
 
@@ -1,54 +1,3 @@
1
- const jetpack = require('fs-jetpack');
1
+ const run = require('./runner.js');
2
2
 
3
- /**
4
- * Daily cron job runner
5
- *
6
- * Executes all daily jobs from:
7
- * 1. BEM core jobs (src/manager/cron/daily/)
8
- * 2. Custom project jobs (functions/hooks/cron/daily/)
9
- */
10
- module.exports = async ({ Manager, assistant, context, libraries }) => {
11
- // Set log prefix
12
- assistant.setLogPrefix('cron/daily()');
13
-
14
- // Log
15
- assistant.log('Starting...');
16
-
17
- // Load BEM jobs
18
- await loadAndExecuteJobs(`${__dirname}/daily`, Manager, context);
19
-
20
- // Load custom jobs
21
- await loadAndExecuteJobs(`${Manager.cwd}/hooks/cron/daily`, Manager, context);
22
- };
23
-
24
- async function loadAndExecuteJobs(jobsPath, Manager, context) {
25
- const jobs = jetpack.list(jobsPath) || [];
26
-
27
- // Log
28
- Manager.assistant.log(`Located ${jobs.length} jobs @ ${jobsPath}...`);
29
-
30
- for (const job of jobs) {
31
- // Create new assistant for each job
32
- const assistant = Manager.Assistant();
33
-
34
- // Load job
35
- const jobName = job.replace('.js', '');
36
-
37
- // Set log prefix
38
- assistant.setLogPrefix(`cron/daily/${jobName}()`);
39
-
40
- // Log
41
- assistant.log('Starting...');
42
-
43
- try {
44
- // Load and execute job
45
- const handler = require(`${jobsPath}/${job}`);
46
- await handler({ Manager, assistant, context, libraries: Manager.libraries });
47
-
48
- assistant.log('Completed!');
49
- } catch (e) {
50
- assistant.errorify(`Error executing: ${e}`, { code: 500, sentry: true });
51
- throw e;
52
- }
53
- }
54
- }
3
+ module.exports = async (options) => run('daily', options);
@@ -0,0 +1,148 @@
1
+ const powertools = require('node-powertools');
2
+ const { REMINDER_DELAYS, COLLECTION } = require('../../libraries/abandoned-cart-config.js');
3
+ const { sendOrderEmail } = require('../../events/firestore/payments-webhooks/transitions/send-email.js');
4
+
5
+ /**
6
+ * Abandoned cart reminder cron job
7
+ *
8
+ * Queries payments-carts where status is pending and nextReminderAt has passed,
9
+ * sends escalating email reminders, and advances or completes the tracker.
10
+ */
11
+ module.exports = async ({ Manager, assistant, context, libraries }) => {
12
+ const { admin } = libraries;
13
+ const nowUNIX = Math.floor(Date.now() / 1000);
14
+
15
+ // Query all pending carts that are due for a reminder
16
+ const snapshot = await admin.firestore()
17
+ .collection(COLLECTION)
18
+ .where('status', '==', 'pending')
19
+ .where('nextReminderAt', '<=', nowUNIX)
20
+ .get();
21
+
22
+ if (snapshot.empty) {
23
+ assistant.log('No abandoned carts due for reminders');
24
+ return;
25
+ }
26
+
27
+ assistant.log(`Processing ${snapshot.size} abandoned cart reminder(s)...`);
28
+
29
+ let sent = 0;
30
+ let completed = 0;
31
+ let skipped = 0;
32
+
33
+ for (const doc of snapshot.docs) {
34
+ const data = doc.data();
35
+ const uid = data.owner;
36
+ const reminderIndex = data.reminderIndex || 0;
37
+
38
+ try {
39
+ // Fetch user doc for email sending
40
+ const userSnap = await admin.firestore().doc(`users/${uid}`).get();
41
+
42
+ if (!userSnap.exists) {
43
+ assistant.log(`User ${uid} not found, marking cart completed`);
44
+ await markCompleted(doc, admin, nowUNIX);
45
+ skipped++;
46
+ continue;
47
+ }
48
+
49
+ const userDoc = userSnap.data();
50
+
51
+ // Belt-and-suspenders: skip if user already has active paid subscription
52
+ if (userDoc.subscription?.status === 'active'
53
+ && userDoc.subscription?.product?.id !== 'basic') {
54
+ assistant.log(`User ${uid} now has active subscription, marking cart completed`);
55
+ await markCompleted(doc, admin, nowUNIX);
56
+ skipped++;
57
+ continue;
58
+ }
59
+
60
+ // Build checkout URL from cart data
61
+ const checkoutUrl = buildCheckoutUrl(Manager.project.websiteUrl, data);
62
+
63
+ // Send reminder email
64
+ assistant.log(`Sending abandoned cart reminder #${reminderIndex + 1} to uid=${uid}, product=${data.productId}`);
65
+
66
+ sendOrderEmail({
67
+ template: 'main/order/abandoned-cart',
68
+ subject: `Complete your ${data.productId} checkout`,
69
+ categories: ['order/abandoned-cart', `order/abandoned-cart/reminder-${reminderIndex + 1}`],
70
+ userDoc,
71
+ assistant,
72
+ data: {
73
+ abandonedCart: {
74
+ productId: data.productId,
75
+ type: data.type,
76
+ frequency: data.frequency,
77
+ reminderNumber: reminderIndex + 1,
78
+ totalReminders: REMINDER_DELAYS.length,
79
+ checkoutUrl: checkoutUrl,
80
+ },
81
+ },
82
+ });
83
+
84
+ sent++;
85
+
86
+ // Advance to next reminder or mark completed if this was the last one
87
+ const nextIndex = reminderIndex + 1;
88
+
89
+ if (nextIndex >= REMINDER_DELAYS.length) {
90
+ assistant.log(`Last reminder sent for uid=${uid}, marking cart completed`);
91
+ await markCompleted(doc, admin, nowUNIX);
92
+ completed++;
93
+ } else {
94
+ const now = powertools.timestamp(new Date(), { output: 'string' });
95
+ const updatedNowUNIX = powertools.timestamp(now, { output: 'unix' });
96
+
97
+ await doc.ref.set({
98
+ reminderIndex: nextIndex,
99
+ nextReminderAt: updatedNowUNIX + REMINDER_DELAYS[nextIndex],
100
+ metadata: {
101
+ updated: {
102
+ timestamp: now,
103
+ timestampUNIX: updatedNowUNIX,
104
+ },
105
+ },
106
+ }, { merge: true });
107
+
108
+ assistant.log(`Advanced uid=${uid} to reminder index ${nextIndex}, next at ${updatedNowUNIX + REMINDER_DELAYS[nextIndex]}`);
109
+ }
110
+ } catch (e) {
111
+ assistant.error(`Error processing abandoned cart for uid=${uid}: ${e.message}`, e);
112
+ // Continue to next document
113
+ }
114
+ }
115
+
116
+ assistant.log(`Completed! (${sent} sent, ${completed} completed, ${skipped} skipped)`);
117
+ };
118
+
119
+ /**
120
+ * Mark a cart document as completed
121
+ */
122
+ async function markCompleted(doc, admin, nowUNIX) {
123
+ const now = powertools.timestamp(new Date(), { output: 'string' });
124
+
125
+ await doc.ref.set({
126
+ status: 'completed',
127
+ metadata: {
128
+ updated: {
129
+ timestamp: now,
130
+ timestampUNIX: nowUNIX,
131
+ },
132
+ },
133
+ }, { merge: true });
134
+ }
135
+
136
+ /**
137
+ * Build checkout URL from cart data
138
+ */
139
+ function buildCheckoutUrl(baseUrl, data) {
140
+ const url = new URL('/payment/checkout', baseUrl);
141
+ url.searchParams.set('product', data.productId);
142
+
143
+ if (data.frequency) {
144
+ url.searchParams.set('frequency', data.frequency);
145
+ }
146
+
147
+ return url.toString();
148
+ }
@@ -0,0 +1,3 @@
1
+ const run = require('./runner.js');
2
+
3
+ module.exports = async (options) => run('frequent', options);
@@ -0,0 +1,60 @@
1
+ const jetpack = require('fs-jetpack');
2
+
3
+ /**
4
+ * Shared cron job runner
5
+ *
6
+ * Discovers and executes all .js job files from:
7
+ * 1. BEM core jobs directory
8
+ * 2. Custom project hooks directory
9
+ *
10
+ * @param {string} name - Cron schedule name (e.g., 'daily', 'frequent')
11
+ * @param {object} options
12
+ * @param {object} options.Manager - Manager instance
13
+ * @param {object} options.assistant - Assistant instance
14
+ * @param {object} options.context - Cloud Function context
15
+ */
16
+ module.exports = async function run(name, { Manager, assistant, context }) {
17
+ // Set log prefix
18
+ assistant.setLogPrefix(`cron/${name}()`);
19
+
20
+ // Log
21
+ assistant.log('Starting...');
22
+
23
+ // Load BEM jobs
24
+ await loadAndExecuteJobs(name, `${__dirname}/${name}`, Manager, context);
25
+
26
+ // Load custom jobs
27
+ await loadAndExecuteJobs(name, `${Manager.cwd}/hooks/cron/${name}`, Manager, context);
28
+ };
29
+
30
+ async function loadAndExecuteJobs(name, jobsPath, Manager, context) {
31
+ const jobs = jetpack.list(jobsPath) || [];
32
+
33
+ // Log
34
+ Manager.assistant.log(`Located ${jobs.length} jobs @ ${jobsPath}...`);
35
+
36
+ for (const job of jobs) {
37
+ // Create new assistant for each job
38
+ const assistant = Manager.Assistant();
39
+
40
+ // Load job
41
+ const jobName = job.replace('.js', '');
42
+
43
+ // Set log prefix
44
+ assistant.setLogPrefix(`cron/${name}/${jobName}()`);
45
+
46
+ // Log
47
+ assistant.log('Starting...');
48
+
49
+ try {
50
+ // Load and execute job
51
+ const handler = require(`${jobsPath}/${job}`);
52
+ await handler({ Manager, assistant, context, libraries: Manager.libraries });
53
+
54
+ assistant.log('Completed!');
55
+ } catch (e) {
56
+ assistant.errorify(`Error executing: ${e}`, { code: 500, sentry: true });
57
+ throw e;
58
+ }
59
+ }
60
+ }