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/cli/index.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
1
3
|
const argv = require('yargs').argv;
|
|
2
4
|
const _ = require('lodash');
|
|
3
5
|
|
|
6
|
+
// Abort if running from ~/node_modules (accidental home directory install)
|
|
7
|
+
const _homeDir = os.homedir();
|
|
8
|
+
if (__dirname.startsWith(path.join(_homeDir, 'node_modules'))) {
|
|
9
|
+
console.error(`\nERROR: BEM is running from ~/node_modules (home directory install).`);
|
|
10
|
+
console.error(`This shadows the local project copy. Fix:`);
|
|
11
|
+
console.error(` rm -rf ~/node_modules ~/package.json ~/package-lock.json\n`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
// Import commands
|
|
5
16
|
const VersionCommand = require('./commands/version');
|
|
6
17
|
const ClearCommand = require('./commands/clear');
|
|
@@ -10,7 +21,7 @@ const InstallCommand = require('./commands/install');
|
|
|
10
21
|
const ServeCommand = require('./commands/serve');
|
|
11
22
|
const DeployCommand = require('./commands/deploy');
|
|
12
23
|
const TestCommand = require('./commands/test');
|
|
13
|
-
const
|
|
24
|
+
const EmulatorCommand = require('./commands/emulator');
|
|
14
25
|
const CleanCommand = require('./commands/clean');
|
|
15
26
|
const IndexesCommand = require('./commands/indexes');
|
|
16
27
|
const WatchCommand = require('./commands/watch');
|
|
@@ -95,9 +106,9 @@ Main.prototype.process = async function (args) {
|
|
|
95
106
|
return await cmd.execute();
|
|
96
107
|
}
|
|
97
108
|
|
|
98
|
-
//
|
|
99
|
-
if (self.options['
|
|
100
|
-
const cmd = new
|
|
109
|
+
// Emulator (keep-alive mode)
|
|
110
|
+
if (self.options['emulator'] || self.options['emulators']) {
|
|
111
|
+
const cmd = new EmulatorCommand(self);
|
|
101
112
|
return await cmd.execute();
|
|
102
113
|
}
|
|
103
114
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const fetch = require('wonderful-fetch');
|
|
2
|
-
const moment = require('moment');
|
|
3
1
|
const { FieldValue } = require('firebase-admin/firestore');
|
|
4
2
|
|
|
5
3
|
const MAX_RETRIES = 3;
|
|
@@ -15,7 +13,9 @@ const RETRY_DELAY_MS = 1000;
|
|
|
15
13
|
* - Checks if user doc already exists (auth.uid) → skips if exists (handles test accounts, provider linking)
|
|
16
14
|
* - Batch writes user doc + increment count atomically
|
|
17
15
|
* - Retries up to 3 times with exponential backoff on failure
|
|
18
|
-
*
|
|
16
|
+
*
|
|
17
|
+
* Non-critical work (name inference, welcome emails, marketing contact) is handled
|
|
18
|
+
* by the user/signup endpoint, which the frontend calls after account creation.
|
|
19
19
|
*/
|
|
20
20
|
module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
21
21
|
const startTime = Date.now();
|
|
@@ -71,42 +71,13 @@ module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
|
71
71
|
await batch.commit();
|
|
72
72
|
}, MAX_RETRIES, RETRY_DELAY_MS);
|
|
73
73
|
|
|
74
|
-
assistant.log(`onCreate: Successfully created user doc for ${user.uid}`);
|
|
74
|
+
assistant.log(`onCreate: Successfully created user doc for ${user.uid} (${Date.now() - startTime}ms)`);
|
|
75
75
|
} catch (error) {
|
|
76
76
|
assistant.error(`onCreate: Failed to create user doc after ${MAX_RETRIES} retries:`, error);
|
|
77
77
|
|
|
78
78
|
// Don't reject - the user was already created in Auth
|
|
79
|
-
// The user
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Send emails in dev/production, or in test mode if TEST_EXTENDED_MODE=true
|
|
84
|
-
// Note: Must be passed to the emulator
|
|
85
|
-
const shouldSendEmails = !assistant.isTesting() || process.env.TEST_EXTENDED_MODE;
|
|
86
|
-
|
|
87
|
-
if (!shouldSendEmails) {
|
|
88
|
-
assistant.log(`onCreate: Skipping emails/SendGrid (BEM_TESTING=true, TEST_EXTENDED_MODE not set)`);
|
|
89
|
-
} else {
|
|
90
|
-
assistant.log(`onCreate: Sending emails/adding to SendGrid for ${user.uid}`);
|
|
91
|
-
|
|
92
|
-
// Add to marketing lists (SendGrid + Beehiiv) via centralized endpoint
|
|
93
|
-
fetch(`${Manager.project.apiUrl}/backend-manager/marketing/contact`, {
|
|
94
|
-
method: 'POST',
|
|
95
|
-
response: 'json',
|
|
96
|
-
body: {
|
|
97
|
-
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
98
|
-
email: user.email,
|
|
99
|
-
source: 'auth:on-create',
|
|
100
|
-
},
|
|
101
|
-
}).catch(e => assistant.error('onCreate: add-marketing-contact failed:', e));
|
|
102
|
-
|
|
103
|
-
// Send welcome emails (non-blocking, don't fail on error)
|
|
104
|
-
sendWelcomeEmail(Manager, assistant, user).catch(e => assistant.error('onCreate: sendWelcomeEmail failed:', e));
|
|
105
|
-
sendCheckupEmail(Manager, assistant, user).catch(e => assistant.error('onCreate: sendCheckupEmail failed:', e));
|
|
106
|
-
sendFeedbackEmail(Manager, assistant, user).catch(e => assistant.error('onCreate: sendFeedbackEmail failed:', e));
|
|
79
|
+
// The user/signup endpoint will handle creating the doc if it's missing
|
|
107
80
|
}
|
|
108
|
-
|
|
109
|
-
assistant.log(`onCreate: Completed for ${user.uid} (${Date.now() - startTime}ms)`);
|
|
110
81
|
};
|
|
111
82
|
|
|
112
83
|
/**
|
|
@@ -133,127 +104,3 @@ async function retryBatchWrite(assistant, fn, maxRetries, delayMs) {
|
|
|
133
104
|
|
|
134
105
|
throw lastError; // All retries failed
|
|
135
106
|
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Send welcome email (immediate)
|
|
139
|
-
*/
|
|
140
|
-
function sendWelcomeEmail(Manager, assistant, user) {
|
|
141
|
-
return fetch(`${Manager.project.apiUrl}/backend-manager/admin/email`, {
|
|
142
|
-
method: 'POST',
|
|
143
|
-
response: 'json',
|
|
144
|
-
body: {
|
|
145
|
-
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
146
|
-
to: [{ email: user.email }],
|
|
147
|
-
categories: ['account/welcome'],
|
|
148
|
-
subject: `Welcome to ${Manager.config.brand.name}!`,
|
|
149
|
-
template: 'd-b7f8da3c98ad49a2ad1e187f3a67b546',
|
|
150
|
-
group: 25928,
|
|
151
|
-
copy: false,
|
|
152
|
-
ensureUnique: true,
|
|
153
|
-
data: {
|
|
154
|
-
email: {
|
|
155
|
-
preview: `Welcome aboard! I'm Ian, the CEO and founder of ${Manager.config.brand.name}. I'm here to ensure your journey with us gets off to a great start.`,
|
|
156
|
-
},
|
|
157
|
-
body: {
|
|
158
|
-
title: `Welcome to ${Manager.config.brand.name}!`,
|
|
159
|
-
message: `
|
|
160
|
-
Welcome aboard!
|
|
161
|
-
<br><br>
|
|
162
|
-
I'm Ian, the founder and CEO of <strong>${Manager.config.brand.name}</strong>, and I'm thrilled to have you with us.
|
|
163
|
-
Your journey begins today, and we are committed to supporting you every step of the way.
|
|
164
|
-
<br><br>
|
|
165
|
-
We are dedicated to ensuring your experience is exceptional.
|
|
166
|
-
Feel free to reply directly to this email with any questions you may have.
|
|
167
|
-
<br><br>
|
|
168
|
-
Thank you for choosing <strong>${Manager.config.brand.name}</strong>. Here's to new beginnings!
|
|
169
|
-
`,
|
|
170
|
-
},
|
|
171
|
-
signoff: {
|
|
172
|
-
type: 'personal',
|
|
173
|
-
name: 'Ian Wiedenman, CEO',
|
|
174
|
-
url: `https://ianwiedenman.com?utm_source=welcome-email&utm_medium=email&utm_campaign=${Manager.config.app.id}`,
|
|
175
|
-
urlText: '@ianwieds',
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
})
|
|
180
|
-
.then((json) => {
|
|
181
|
-
assistant.log('sendWelcomeEmail(): Success', json);
|
|
182
|
-
return json;
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Send checkup email (7 days after signup)
|
|
188
|
-
*/
|
|
189
|
-
function sendCheckupEmail(Manager, assistant, user) {
|
|
190
|
-
return fetch(`${Manager.project.apiUrl}/backend-manager/admin/email`, {
|
|
191
|
-
method: 'POST',
|
|
192
|
-
response: 'json',
|
|
193
|
-
body: {
|
|
194
|
-
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
195
|
-
to: [{ email: user.email }],
|
|
196
|
-
categories: ['account/checkup'],
|
|
197
|
-
subject: `How's your experience with ${Manager.config.brand.name}?`,
|
|
198
|
-
template: 'd-b7f8da3c98ad49a2ad1e187f3a67b546',
|
|
199
|
-
group: 25928,
|
|
200
|
-
copy: false,
|
|
201
|
-
ensureUnique: true,
|
|
202
|
-
sendAt: moment().add(7, 'days').unix(),
|
|
203
|
-
data: {
|
|
204
|
-
email: {
|
|
205
|
-
preview: `Checking in from ${Manager.config.brand.name} to see how things are going. Let us know if you have any questions or feedback!`,
|
|
206
|
-
},
|
|
207
|
-
body: {
|
|
208
|
-
title: `How's everything going?`,
|
|
209
|
-
message: `
|
|
210
|
-
Hi there,
|
|
211
|
-
<br><br>
|
|
212
|
-
It's Ian again from <strong>${Manager.config.brand.name}</strong>. Just checking in to see how things are going for you.
|
|
213
|
-
<br><br>
|
|
214
|
-
Have you had a chance to explore all our features? Any questions or feedback for us?
|
|
215
|
-
<br><br>
|
|
216
|
-
We're always here to help, so don't hesitate to reach out. Just reply to this email and we'll get back to you as soon as possible.
|
|
217
|
-
<br><br>
|
|
218
|
-
Thank you for choosing <strong>${Manager.config.brand.name}</strong>. Here's to new beginnings!
|
|
219
|
-
`,
|
|
220
|
-
},
|
|
221
|
-
signoff: {
|
|
222
|
-
type: 'personal',
|
|
223
|
-
name: 'Ian Wiedenman, CEO',
|
|
224
|
-
url: `https://ianwiedenman.com?utm_source=checkup-email&utm_medium=email&utm_campaign=${Manager.config.app.id}`,
|
|
225
|
-
urlText: '@ianwieds',
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
})
|
|
230
|
-
.then((json) => {
|
|
231
|
-
assistant.log('sendCheckupEmail(): Success', json);
|
|
232
|
-
return json;
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Send feedback email (14 days after signup)
|
|
238
|
-
*/
|
|
239
|
-
function sendFeedbackEmail(Manager, assistant, user) {
|
|
240
|
-
return fetch(`${Manager.project.apiUrl}/backend-manager/admin/email`, {
|
|
241
|
-
method: 'POST',
|
|
242
|
-
response: 'json',
|
|
243
|
-
body: {
|
|
244
|
-
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
245
|
-
to: [{ email: user.email }],
|
|
246
|
-
categories: ['engagement/feedback'],
|
|
247
|
-
subject: `Want to share your feedback about ${Manager.config.brand.name}?`,
|
|
248
|
-
template: 'd-c1522214c67b47058669acc5a81ed663',
|
|
249
|
-
group: 25928,
|
|
250
|
-
copy: false,
|
|
251
|
-
ensureUnique: true,
|
|
252
|
-
sendAt: moment().add(14, 'days').unix(),
|
|
253
|
-
},
|
|
254
|
-
})
|
|
255
|
-
.then((json) => {
|
|
256
|
-
assistant.log('sendFeedbackEmail(): Success', json);
|
|
257
|
-
return json;
|
|
258
|
-
});
|
|
259
|
-
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment analytics tracking
|
|
3
|
+
* Fires server-side events for GA4, Meta Conversions API, and TikTok Events API
|
|
4
|
+
*
|
|
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
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 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)
|
|
22
|
+
*/
|
|
23
|
+
function trackPayment({ category, transitionName, unified, uid, processor, assistant }) {
|
|
24
|
+
const Manager = assistant.Manager;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Resolve the analytics event to fire based on transition
|
|
28
|
+
const event = resolvePaymentEvent(category, transitionName, unified, Manager.config);
|
|
29
|
+
|
|
30
|
+
if (!event) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
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
|
+
});
|
|
52
|
+
|
|
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
|
|
56
|
+
|
|
57
|
+
// TODO: TikTok Events API
|
|
58
|
+
// Event name: event.tiktok (e.g., 'CompletePayment', 'Subscribe')
|
|
59
|
+
// https://business-api.tiktok.com/portal/docs?id=1771100865818625
|
|
60
|
+
} catch (e) {
|
|
61
|
+
assistant.error(`trackPayment failed: ${e.message}`, e);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve which analytics event to fire based on transition + unified data
|
|
67
|
+
* Returns null if the transition doesn't warrant an analytics event
|
|
68
|
+
*/
|
|
69
|
+
function resolvePaymentEvent(category, transitionName, unified, config) {
|
|
70
|
+
if (category === 'subscription') {
|
|
71
|
+
return resolveSubscriptionEvent(transitionName, unified, config);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (category === 'one-time') {
|
|
75
|
+
return resolveOneTimeEvent(transitionName, unified, config);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Map subscription transitions to analytics events
|
|
83
|
+
*/
|
|
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
|
+
};
|
|
106
|
+
}
|
|
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
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
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,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Map one-time transitions to analytics events
|
|
145
|
+
*/
|
|
146
|
+
function resolveOneTimeEvent(transitionName, unified, config) {
|
|
147
|
+
if (transitionName !== 'purchase-completed') {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
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;
|
|
155
|
+
|
|
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
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { trackPayment };
|