backend-manager 5.0.122 → 5.0.124

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 (45) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/CLAUDE.md +18 -19
  3. package/README.md +7 -7
  4. package/package.json +1 -1
  5. package/src/manager/cron/daily/reset-usage.js +79 -73
  6. package/src/manager/cron/daily.js +2 -53
  7. package/src/manager/cron/frequent/abandoned-carts.js +148 -0
  8. package/src/manager/cron/frequent.js +3 -0
  9. package/src/manager/cron/runner.js +60 -0
  10. package/src/manager/events/firestore/payments-disputes/on-write.js +358 -0
  11. package/src/manager/events/firestore/payments-webhooks/analytics.js +245 -121
  12. package/src/manager/events/firestore/payments-webhooks/on-write.js +32 -2
  13. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +11 -34
  14. package/src/manager/helpers/analytics.js +2 -2
  15. package/src/manager/helpers/usage.js +44 -20
  16. package/src/manager/helpers/user.js +2 -1
  17. package/src/manager/index.js +10 -0
  18. package/src/manager/libraries/abandoned-cart-config.js +12 -0
  19. package/src/manager/libraries/email.js +5 -5
  20. package/src/manager/libraries/openai.js +76 -7
  21. package/src/manager/libraries/payment/discount-codes.js +40 -0
  22. package/src/manager/libraries/recaptcha.js +36 -0
  23. package/src/manager/routes/app/get.js +1 -1
  24. package/src/manager/routes/marketing/contact/post.js +11 -29
  25. package/src/manager/routes/payments/discount/get.js +22 -0
  26. package/src/manager/routes/payments/dispute-alert/post.js +93 -0
  27. package/src/manager/routes/payments/dispute-alert/processors/chargeblast.js +43 -0
  28. package/src/manager/routes/payments/intent/post.js +29 -0
  29. package/src/manager/routes/payments/intent/processors/chargebee.js +59 -7
  30. package/src/manager/routes/payments/intent/processors/stripe.js +55 -7
  31. package/src/manager/routes/test/usage/post.js +10 -6
  32. package/src/manager/schemas/payments/discount/get.js +9 -0
  33. package/src/manager/schemas/payments/dispute-alert/post.js +6 -0
  34. package/src/manager/schemas/payments/intent/post.js +16 -0
  35. package/src/test/runner.js +14 -4
  36. package/src/test/test-accounts.js +18 -0
  37. package/templates/backend-manager-config.json +7 -1
  38. package/templates/firestore.rules +9 -1
  39. package/test/_legacy/usage.js +5 -5
  40. package/test/routes/marketing/contact.js +3 -2
  41. package/test/routes/payments/discount.js +80 -0
  42. package/test/routes/payments/dispute-alert.js +271 -0
  43. package/test/routes/payments/intent.js +60 -0
  44. package/test/routes/test/usage.js +134 -30
  45. package/test/rules/payments-carts.js +371 -0
@@ -1,171 +1,295 @@
1
+ const fetch = require('wonderful-fetch');
2
+
1
3
  /**
2
4
  * Payment analytics tracking
3
- * Fires server-side events for GA4, Meta Conversions API, and TikTok Events API
5
+ * Fires server-side events independently to GA4, Meta Conversions API, and TikTok Events API
6
+ *
7
+ * Two independent concerns:
8
+ * 1. Transition events (mutually exclusive, one per webhook):
9
+ * new-subscription (no trial) → purchase
10
+ * new-subscription (trial) → start_trial
11
+ * payment-recovered → purchase (recurring)
12
+ * purchase-completed → purchase (one-time)
4
13
  *
5
- * Maps transitions to standard platform events:
6
- * new-subscription (no trial) → purchase / Purchase / CompletePayment
7
- * new-subscription (trial) → start_trial / StartTrial / Subscribe
8
- * payment-recovered → purchase / Subscribe / Subscribe (recurring)
9
- * purchase-completed → purchase / Purchase / CompletePayment
14
+ * 2. Payment events (fire whenever money changes hands, including renewals):
15
+ * subscription renewal → purchase (recurring)
10
16
  */
11
17
 
12
18
  /**
13
19
  * Track payment events across analytics platforms (non-blocking)
14
- *
15
- * @param {object} options
16
- * @param {string} options.category - 'subscription' or 'one-time'
17
- * @param {string} options.transitionName - Detected transition (e.g., 'new-subscription', 'purchase-completed')
18
- * @param {object} options.unified - Unified subscription or one-time object
19
- * @param {string} options.uid - User ID
20
- * @param {string} options.processor - Payment processor (e.g., 'stripe', 'paypal')
21
- * @param {object} options.assistant - Assistant instance (Manager derived via assistant.Manager)
20
+ * Fires GA4, Meta, and TikTok independently with per-platform payloads
22
21
  */
23
- function trackPayment({ category, transitionName, unified, uid, processor, assistant }) {
22
+ function trackPayment({ category, transitionName, eventType, unified, uid, processor, assistant }) {
24
23
  const Manager = assistant.Manager;
24
+ const config = Manager.config;
25
25
 
26
26
  try {
27
- // Resolve the analytics event to fire based on transition
28
- const event = resolvePaymentEvent(category, transitionName, unified, Manager.config);
27
+ // Resolve what kind of payment event this is
28
+ const resolved = resolvePaymentEvent(category, transitionName, eventType, unified);
29
29
 
30
- if (!event) {
30
+ if (!resolved) {
31
+ assistant.log(`trackPayment: skipped — no trackable event (category=${category}, transition=${transitionName || 'null'}, eventType=${eventType})`);
31
32
  return;
32
33
  }
33
34
 
34
- assistant.log(`trackPayment: event=${event.ga4}, value=${event.value}, currency=${event.currency}, product=${event.productId}, uid=${uid}`);
35
-
36
- // GA4 via Measurement Protocol
37
- Manager.Analytics({ assistant, uuid: uid }).event(event.ga4, {
38
- transaction_id: event.transactionId,
39
- value: event.value,
40
- currency: event.currency,
41
- items: [{
42
- item_id: event.productId,
43
- item_name: event.productName,
44
- price: event.value,
45
- quantity: 1,
46
- }],
47
- payment_processor: processor,
48
- payment_frequency: event.frequency,
49
- is_trial: event.isTrial,
50
- is_recurring: event.isRecurring,
51
- });
35
+ const currency = config.payment?.currency || 'USD';
52
36
 
53
- // TODO: Meta Conversions API
54
- // Event name: event.meta (e.g., 'Purchase', 'StartTrial', 'Subscribe')
55
- // https://developers.facebook.com/docs/marketing-api/conversions-api
37
+ assistant.log(`trackPayment: reason=${resolved.reason}, value=${resolved.value}, currency=${currency}, product=${resolved.productId}, uid=${uid}, processor=${processor}`);
56
38
 
57
- // TODO: TikTok Events API
58
- // Event name: event.tiktok (e.g., 'CompletePayment', 'Subscribe')
59
- // https://business-api.tiktok.com/portal/docs?id=1771100865818625
39
+ // Fire each platform independently (non-blocking, errors isolated)
40
+ fireGA4({ resolved, currency, uid, processor, assistant, Manager });
41
+ fireMeta({ resolved, currency, uid, processor, assistant, config });
42
+ fireTikTok({ resolved, currency, uid, processor, assistant, config });
60
43
  } catch (e) {
61
44
  assistant.error(`trackPayment failed: ${e.message}`, e);
62
45
  }
63
46
  }
64
47
 
48
+ // ---------------------------------------------------------------------------
49
+ // Event resolution
50
+ // ---------------------------------------------------------------------------
51
+
65
52
  /**
66
- * Resolve which analytics event to fire based on transition + unified data
67
- * Returns null if the transition doesn't warrant an analytics event
53
+ * Determine what kind of payment event occurred and extract common fields
54
+ * Returns null if nothing should be tracked
68
55
  */
69
- function resolvePaymentEvent(category, transitionName, unified, config) {
56
+ function resolvePaymentEvent(category, transitionName, eventType, unified) {
57
+ const productId = unified.product?.id;
58
+ const productName = unified.product?.name;
59
+ const frequency = unified.payment?.frequency || null;
60
+ const isTrial = unified.trial?.claimed === true;
61
+ const resourceId = unified.payment?.resourceId;
62
+ const price = unified.payment?.price || 0;
63
+
64
+ const base = { productId, productName, frequency, resourceId, isTrial };
65
+
66
+ // --- Subscription transitions ---
70
67
  if (category === 'subscription') {
71
- return resolveSubscriptionEvent(transitionName, unified, config);
68
+ if (transitionName === 'new-subscription' && isTrial) {
69
+ return { ...base, reason: 'trial-started', value: 0, isRecurring: false };
70
+ }
71
+
72
+ if (transitionName === 'new-subscription') {
73
+ return { ...base, reason: 'first-purchase', value: price, isRecurring: false };
74
+ }
75
+
76
+ if (transitionName === 'payment-recovered') {
77
+ return { ...base, reason: 'payment-recovered', value: price, isRecurring: true };
78
+ }
79
+
80
+ // No transition but a payment event fired (renewal)
81
+ if (!transitionName && isPaymentEvent(eventType) && price > 0) {
82
+ return { ...base, reason: 'renewal', value: price, isRecurring: true };
83
+ }
84
+
85
+ return null;
72
86
  }
73
87
 
88
+ // --- One-time transitions ---
74
89
  if (category === 'one-time') {
75
- return resolveOneTimeEvent(transitionName, unified, config);
90
+ if (transitionName === 'purchase-completed') {
91
+ return { ...base, reason: 'one-time-purchase', value: price, isRecurring: false, productId: productId || 'unknown', productName: productName || 'Unknown' };
92
+ }
93
+
94
+ return null;
76
95
  }
77
96
 
78
97
  return null;
79
98
  }
80
99
 
81
100
  /**
82
- * Map subscription transitions to analytics events
101
+ * Check if a webhook event type represents a payment being made
83
102
  */
84
- function resolveSubscriptionEvent(transitionName, unified, config) {
85
- const productId = unified.product?.id;
86
- const productName = unified.product?.name;
87
- const frequency = unified.payment?.frequency;
88
- const isTrial = unified.trial?.claimed === true;
89
- const resourceId = unified.payment?.resourceId;
90
- const price = unified.payment?.price || 0;
91
-
92
- if (transitionName === 'new-subscription' && isTrial) {
93
- return {
94
- ga4: 'start_trial',
95
- meta: 'StartTrial',
96
- tiktok: 'Subscribe',
97
- value: 0,
98
- currency: config.payment?.currency || 'USD',
99
- productId,
100
- productName,
101
- frequency,
102
- isTrial: true,
103
- isRecurring: false,
104
- transactionId: resourceId,
105
- };
103
+ function isPaymentEvent(eventType) {
104
+ if (!eventType) {
105
+ return false;
106
106
  }
107
107
 
108
- if (transitionName === 'new-subscription') {
109
- return {
110
- ga4: 'purchase',
111
- meta: 'Purchase',
112
- tiktok: 'CompletePayment',
113
- value: price,
114
- currency: config.payment?.currency || 'USD',
115
- productId,
116
- productName,
117
- frequency,
118
- isTrial: false,
119
- isRecurring: false,
120
- transactionId: resourceId,
121
- };
108
+ return [
109
+ // PayPal
110
+ 'PAYMENT.SALE.COMPLETED',
111
+ // Stripe
112
+ 'invoice.payment_succeeded',
113
+ 'invoice.paid',
114
+ ].includes(eventType);
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // GA4 — Measurement Protocol
119
+ // ---------------------------------------------------------------------------
120
+
121
+ /**
122
+ * Fire GA4 event via Manager.Analytics (Measurement Protocol)
123
+ * https://developers.google.com/analytics/devguides/collection/protocol/ga4
124
+ */
125
+ function fireGA4({ resolved, currency, uid, processor, assistant, Manager }) {
126
+ try {
127
+ // Map reason → GA4 event name
128
+ const eventName = resolved.reason === 'trial-started' ? 'start_trial' : 'purchase';
129
+
130
+ Manager.Analytics({ assistant, uuid: uid }).event(eventName, {
131
+ transaction_id: resolved.resourceId,
132
+ value: resolved.value,
133
+ currency: currency,
134
+ items: [{
135
+ item_id: resolved.productId,
136
+ item_name: resolved.productName,
137
+ price: resolved.value,
138
+ quantity: 1,
139
+ }],
140
+ payment_processor: processor,
141
+ payment_frequency: resolved.frequency,
142
+ is_trial: resolved.isTrial,
143
+ is_recurring: resolved.isRecurring,
144
+ });
145
+
146
+ assistant.log(`trackPayment [GA4]: event=${eventName}, value=${resolved.value}, product=${resolved.productId}, uid=${uid}`);
147
+ } catch (e) {
148
+ assistant.error(`trackPayment [GA4] failed: ${e.message}`, e);
122
149
  }
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Meta — Conversions API
154
+ // ---------------------------------------------------------------------------
155
+
156
+ // Meta event name mapping
157
+ const META_EVENTS = {
158
+ 'trial-started': 'StartTrial',
159
+ 'first-purchase': 'Purchase',
160
+ 'payment-recovered': 'Subscribe',
161
+ 'renewal': 'Subscribe',
162
+ 'one-time-purchase': 'Purchase',
163
+ };
164
+
165
+ /**
166
+ * Fire Meta Conversions API event
167
+ * https://developers.facebook.com/docs/marketing-api/conversions-api
168
+ */
169
+ function fireMeta({ resolved, currency, uid, processor, assistant, config }) {
170
+ try {
171
+ const pixelId = config.meta?.pixelId;
172
+ const accessToken = process.env.META_ACCESS_TOKEN;
123
173
 
124
- if (transitionName === 'payment-recovered') {
125
- return {
126
- ga4: 'purchase',
127
- meta: 'Subscribe',
128
- tiktok: 'Subscribe',
129
- value: price,
130
- currency: config.payment?.currency || 'USD',
131
- productId,
132
- productName,
133
- frequency,
134
- isTrial: false,
135
- isRecurring: true,
136
- transactionId: resourceId,
174
+ if (!pixelId || !accessToken) {
175
+ return;
176
+ }
177
+
178
+ const eventName = META_EVENTS[resolved.reason];
179
+
180
+ if (!eventName) {
181
+ return;
182
+ }
183
+
184
+ const payload = {
185
+ data: [{
186
+ event_name: eventName,
187
+ event_time: Math.floor(Date.now() / 1000),
188
+ action_source: 'website',
189
+ user_data: {
190
+ external_id: uid,
191
+ },
192
+ custom_data: {
193
+ value: resolved.value,
194
+ currency: currency,
195
+ content_ids: [resolved.productId],
196
+ content_name: resolved.productName,
197
+ content_type: 'product',
198
+ payment_processor: processor,
199
+ is_recurring: resolved.isRecurring,
200
+ },
201
+ }],
137
202
  };
138
- }
139
203
 
140
- return null;
204
+ fetch(`https://graph.facebook.com/v21.0/${pixelId}/events?access_token=${accessToken}`, {
205
+ method: 'post',
206
+ response: 'json',
207
+ body: payload,
208
+ timeout: 30000,
209
+ tries: 2,
210
+ })
211
+ .then(() => {
212
+ assistant.log(`trackPayment [Meta]: event=${eventName}, value=${resolved.value}, product=${resolved.productId}, uid=${uid}`);
213
+ })
214
+ .catch((e) => {
215
+ assistant.error(`trackPayment [Meta] failed: ${e.message}`, e);
216
+ });
217
+ } catch (e) {
218
+ assistant.error(`trackPayment [Meta] failed: ${e.message}`, e);
219
+ }
141
220
  }
142
221
 
222
+ // ---------------------------------------------------------------------------
223
+ // TikTok — Events API
224
+ // ---------------------------------------------------------------------------
225
+
226
+ // TikTok event name mapping
227
+ const TIKTOK_EVENTS = {
228
+ 'trial-started': 'Subscribe',
229
+ 'first-purchase': 'CompletePayment',
230
+ 'payment-recovered': 'Subscribe',
231
+ 'renewal': 'Subscribe',
232
+ 'one-time-purchase': 'CompletePayment',
233
+ };
234
+
143
235
  /**
144
- * Map one-time transitions to analytics events
236
+ * Fire TikTok Events API event
237
+ * https://business-api.tiktok.com/portal/docs?id=1771100865818625
145
238
  */
146
- function resolveOneTimeEvent(transitionName, unified, config) {
147
- if (transitionName !== 'purchase-completed') {
148
- return null;
149
- }
239
+ function fireTikTok({ resolved, currency, uid, processor, assistant, config }) {
240
+ try {
241
+ const pixelCode = config.tiktok?.pixelCode;
242
+ const accessToken = process.env.TIKTOK_ACCESS_TOKEN;
150
243
 
151
- const productId = unified.product?.id;
152
- const productName = unified.product?.name;
153
- const price = unified.payment?.price || 0;
154
- const resourceId = unified.payment?.resourceId;
244
+ if (!pixelCode || !accessToken) {
245
+ return;
246
+ }
247
+
248
+ const eventName = TIKTOK_EVENTS[resolved.reason];
249
+
250
+ if (!eventName) {
251
+ return;
252
+ }
155
253
 
156
- return {
157
- ga4: 'purchase',
158
- meta: 'Purchase',
159
- tiktok: 'CompletePayment',
160
- value: price,
161
- currency: config.payment?.currency || 'USD',
162
- productId: productId || 'unknown',
163
- productName: productName || 'Unknown',
164
- frequency: null,
165
- isTrial: false,
166
- isRecurring: false,
167
- transactionId: resourceId,
168
- };
254
+ const payload = {
255
+ pixel_code: pixelCode,
256
+ event: eventName,
257
+ event_id: `${uid}-${Date.now()}`,
258
+ timestamp: new Date().toISOString(),
259
+ context: {
260
+ user: {
261
+ external_id: uid,
262
+ },
263
+ },
264
+ properties: {
265
+ value: resolved.value,
266
+ currency: currency,
267
+ content_id: resolved.productId,
268
+ content_name: resolved.productName,
269
+ content_type: 'product',
270
+ description: `${resolved.reason} via ${processor}`,
271
+ },
272
+ };
273
+
274
+ fetch('https://business-api.tiktok.com/open_api/v1.3/event/track/', {
275
+ method: 'post',
276
+ response: 'json',
277
+ headers: {
278
+ 'Access-Token': accessToken,
279
+ },
280
+ body: { data: [payload] },
281
+ timeout: 30000,
282
+ tries: 2,
283
+ })
284
+ .then(() => {
285
+ assistant.log(`trackPayment [TikTok]: event=${eventName}, value=${resolved.value}, product=${resolved.productId}, uid=${uid}`);
286
+ })
287
+ .catch((e) => {
288
+ assistant.error(`trackPayment [TikTok] failed: ${e.message}`, e);
289
+ });
290
+ } catch (e) {
291
+ assistant.error(`trackPayment [TikTok] failed: ${e.message}`, e);
292
+ }
169
293
  }
170
294
 
171
295
  module.exports = { trackPayment };
@@ -194,6 +194,13 @@ async function processPaymentEvent({ category, library, resource, resourceType,
194
194
 
195
195
  assistant.log(`Unified ${category}: product=${unified.product.id}, status=${unified.status}`, unified);
196
196
 
197
+ // Read checkout context from payments-intents (attribution, discount, supplemental)
198
+ let intentData = {};
199
+ if (orderId) {
200
+ const intentDoc = await admin.firestore().doc(`payments-intents/${orderId}`).get();
201
+ intentData = intentDoc.exists ? intentDoc.data() : {};
202
+ }
203
+
197
204
  // Build the order object (single source of truth for handlers + Firestore)
198
205
  const order = {
199
206
  id: orderId,
@@ -203,6 +210,9 @@ async function processPaymentEvent({ category, library, resource, resourceType,
203
210
  processor: processor,
204
211
  resourceId: resourceId,
205
212
  unified: unified,
213
+ attribution: intentData.attribution || {},
214
+ discount: intentData.discount || null,
215
+ supplemental: intentData.supplemental || {},
206
216
  metadata: {
207
217
  created: {
208
218
  timestamp: now,
@@ -243,8 +253,9 @@ async function processPaymentEvent({ category, library, resource, resourceType,
243
253
  }
244
254
 
245
255
  // Track payment analytics (non-blocking)
246
- if (transitionName && shouldRunHandlers) {
247
- trackPayment({ category, transitionName, unified, uid, processor, assistant });
256
+ // Fires independently of transitions — renewals have no transition but still need tracking
257
+ if (shouldRunHandlers) {
258
+ trackPayment({ category, transitionName, eventType, unified, uid, processor, assistant });
248
259
  }
249
260
 
250
261
  // Write unified subscription to user doc (subscriptions only)
@@ -284,6 +295,25 @@ async function processPaymentEvent({ category, library, resource, resourceType,
284
295
  assistant.log(`Updated payments-intents/${orderId}: status=completed`);
285
296
  }
286
297
 
298
+ // Mark abandoned cart as completed (non-blocking, fire-and-forget)
299
+ const { COLLECTION } = require('../../../libraries/abandoned-cart-config.js');
300
+ admin.firestore().doc(`${COLLECTION}/${uid}`).set({
301
+ status: 'completed',
302
+ metadata: {
303
+ updated: {
304
+ timestamp: now,
305
+ timestampUNIX: nowUNIX,
306
+ },
307
+ },
308
+ }, { merge: true })
309
+ .then(() => assistant.log(`Updated ${COLLECTION}/${uid}: status=completed`))
310
+ .catch((e) => {
311
+ // Ignore not-found — cart may not exist for this user
312
+ if (e.code !== 5) {
313
+ assistant.error(`Failed to update ${COLLECTION}/${uid}: ${e.message}`);
314
+ }
315
+ });
316
+
287
317
  return transitionName;
288
318
  }
289
319
 
@@ -1,6 +1,7 @@
1
1
  const fetch = require('wonderful-fetch');
2
2
  const path = require('path');
3
3
  const dns = require('dns').promises;
4
+ const recaptcha = require(path.join(__dirname, '..', '..', '..', '..', '..', 'libraries', 'recaptcha.js'));
4
5
 
5
6
  // Load disposable domains list
6
7
  const DISPOSABLE_DOMAINS = require(path.join(__dirname, '..', '..', '..', '..', '..', 'libraries', 'disposable-domains.json'));
@@ -49,15 +50,17 @@ Module.prototype.main = function () {
49
50
 
50
51
  // Public access protection
51
52
  if (!isAdmin) {
52
- // Verify reCAPTCHA
53
- const recaptchaToken = requestPayload['g-recaptcha-response'];
54
- if (!recaptchaToken) {
55
- return reject(assistant.errorify('reCAPTCHA token required', { code: 400 }));
56
- }
53
+ // Verify reCAPTCHA (skip during automated tests)
54
+ if (!assistant.isTesting()) {
55
+ const recaptchaToken = requestPayload['g-recaptcha-response'];
56
+ if (!recaptchaToken) {
57
+ return reject(assistant.errorify('Request could not be verified', { code: 403 }));
58
+ }
57
59
 
58
- const recaptchaValid = await verifyRecaptcha(recaptchaToken);
59
- if (!recaptchaValid) {
60
- return reject(assistant.errorify('reCAPTCHA verification failed', { code: 400 }));
60
+ const recaptchaValid = await recaptcha.verify(recaptchaToken);
61
+ if (!recaptchaValid) {
62
+ return reject(assistant.errorify('Request could not be verified', { code: 403 }));
63
+ }
61
64
  }
62
65
 
63
66
  // Check rate limit via Usage API
@@ -174,32 +177,6 @@ Module.prototype.main = function () {
174
177
  });
175
178
  };
176
179
 
177
- /**
178
- * Verify Google reCAPTCHA (invisible) token
179
- */
180
- async function verifyRecaptcha(token) {
181
- if (!process.env.RECAPTCHA_SECRET_KEY) {
182
- // Skip verification if no secret configured
183
- return true;
184
- }
185
-
186
- try {
187
- // reCAPTCHA requires form-urlencoded, not JSON
188
- const data = await fetch('https://www.google.com/recaptcha/api/siteverify', {
189
- method: 'post',
190
- response: 'json',
191
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
192
- body: `secret=${process.env.RECAPTCHA_SECRET_KEY}&response=${token}`,
193
- });
194
-
195
- // For v3 invisible reCAPTCHA, check score (0.5+ is typically human)
196
- return data.success && (data.score === undefined || data.score >= 0.5);
197
- } catch (e) {
198
- console.error('reCAPTCHA verification error:', e);
199
- return false;
200
- }
201
- }
202
-
203
180
  /**
204
181
  * Validate email with ZeroBounce API
205
182
  */
@@ -161,8 +161,8 @@ function Analytics(Manager, options) {
161
161
  };
162
162
 
163
163
  // Set id and secret
164
- self.analyticsId = self?.Manager?.config?.google_analytics?.id;
165
- self.analyticsSecret = self?.Manager?.config?.google_analytics?.secret;
164
+ self.analyticsId = self?.Manager?.config?.googleAnalytics?.id;
165
+ self.analyticsSecret = self?.Manager?.config?.googleAnalytics?.secret;
166
166
 
167
167
  // Check if we have the required properties
168
168
  if (!self.analyticsId) {