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,14 +1,48 @@
1
1
  /**
2
2
  * Test: POST /admin/email
3
- * Tests the admin send email endpoint
4
- * Requires admin authentication and SendGrid API key configured
3
+ * Route-level tests: auth, permissions, HTTP status codes, basic send/queue
4
+ *
5
+ * Library-level tests (validation, dedup, recipients, features) are in test/helpers/email.js
5
6
  */
6
7
  module.exports = {
7
- description: 'Admin send email',
8
+ description: 'Admin send email (route)',
8
9
  type: 'group',
9
10
  skip: !process.env.TEST_EXTENDED_MODE ? 'TEST_EXTENDED_MODE env var not set (skipping email tests)' : false,
10
11
  tests: [
11
- // Test 1: Missing subject returns 400 error
12
+ // --- Auth / Permissions ---
13
+
14
+ {
15
+ name: 'unauthenticated-rejected',
16
+ auth: 'none',
17
+ timeout: 15000,
18
+
19
+ async run({ http, assert, config }) {
20
+ const response = await http.post('admin/email', {
21
+ subject: 'Test Email',
22
+ to: [{ email: `_test-receiver@${config.domain}` }],
23
+ });
24
+
25
+ assert.isError(response, 401, 'Send email should fail without authentication');
26
+ },
27
+ },
28
+
29
+ {
30
+ name: 'non-admin-rejected',
31
+ auth: 'basic',
32
+ timeout: 15000,
33
+
34
+ async run({ http, assert, config }) {
35
+ const response = await http.post('admin/email', {
36
+ subject: 'Test Email',
37
+ to: [{ email: `_test-receiver@${config.domain}` }],
38
+ });
39
+
40
+ assert.isError(response, 403, 'Send email should fail for non-admin user');
41
+ },
42
+ },
43
+
44
+ // --- Basic Validation ---
45
+
12
46
  {
13
47
  name: 'missing-subject-rejected',
14
48
  auth: 'admin',
@@ -18,14 +52,14 @@ module.exports = {
18
52
  const response = await http.post('admin/email', {
19
53
  to: [{ email: `_test-receiver@${config.domain}` }],
20
54
  copy: false,
21
- ensureUnique: false,
22
55
  });
23
56
 
24
57
  assert.isError(response, 400, 'Missing subject should return 400');
25
58
  },
26
59
  },
27
60
 
28
- // Test 2: Status 'sent' - Email sent successfully via SendGrid
61
+ // --- Happy Path ---
62
+
29
63
  {
30
64
  name: 'status-sent',
31
65
  auth: 'admin',
@@ -36,7 +70,6 @@ module.exports = {
36
70
  subject: 'BEM Test Email - Status Sent',
37
71
  to: [{ email: `_test-receiver@${config.domain}`, name: 'Test Receiver' }],
38
72
  copy: false,
39
- ensureUnique: false,
40
73
  data: {
41
74
  email: {
42
75
  subject: 'BEM Test Email - Status Sent',
@@ -47,11 +80,10 @@ module.exports = {
47
80
 
48
81
  assert.isSuccess(response, 'Admin should be able to send email');
49
82
  assert.hasProperty(response, 'data.status', 'Response should have status');
50
- assert.equal(response.data.status, 'sent', 'Status should be sent when ensureUnique is false');
83
+ assert.equal(response.data.status, 'sent', 'Status should be sent');
51
84
  },
52
85
  },
53
86
 
54
- // Test 3: Status 'queued' - Email scheduled beyond 71 hours
55
87
  {
56
88
  name: 'status-queued',
57
89
  auth: 'admin',
@@ -64,7 +96,6 @@ module.exports = {
64
96
  subject: 'BEM Test Email - Status Queued',
65
97
  to: [{ email: `_test-receiver@${config.domain}`, name: 'Test Receiver' }],
66
98
  copy: false,
67
- ensureUnique: false,
68
99
  sendAt: sendAt,
69
100
  data: {
70
101
  email: {
@@ -79,85 +110,5 @@ module.exports = {
79
110
  assert.equal(response.data.status, 'queued', 'Status should be queued when sendAt is beyond 71 hours');
80
111
  },
81
112
  },
82
-
83
- // Test 4: Status 'non-unique' - Duplicate email detected
84
- {
85
- name: 'status-non-unique',
86
- auth: 'admin',
87
- timeout: 120000,
88
-
89
- async run({ http, assert, config }) {
90
- const uniqueSubject = `BEM Test Email - Unique Check ${Date.now()}`;
91
-
92
- const response1Promise = http.post('admin/email', {
93
- subject: uniqueSubject,
94
- to: [{ email: `_test-receiver@${config.domain}` }],
95
- copy: false,
96
- ensureUnique: true,
97
- categories: ['bem-test-unique'],
98
- data: {
99
- email: {
100
- subject: uniqueSubject,
101
- body: 'Testing ensureUnique feature.',
102
- },
103
- },
104
- });
105
-
106
- const response2Promise = http.post('admin/email', {
107
- subject: uniqueSubject,
108
- to: [{ email: `_test-receiver@${config.domain}` }],
109
- copy: false,
110
- ensureUnique: true,
111
- categories: ['bem-test-unique'],
112
- data: {
113
- email: {
114
- subject: uniqueSubject,
115
- body: 'Testing ensureUnique feature.',
116
- },
117
- },
118
- });
119
-
120
- const [response1, response2] = await Promise.all([response1Promise, response2Promise]);
121
-
122
- assert.isSuccess(response1, 'First email should succeed');
123
- assert.isSuccess(response2, 'Second email should succeed');
124
-
125
- const statuses = [response1.data.status, response2.data.status].sort();
126
- assert.equal(statuses[0], 'non-unique', 'One email should have status non-unique');
127
- assert.equal(statuses[1], 'sent', 'One email should have status sent');
128
- },
129
- },
130
-
131
- // Test 5: Unauthenticated request fails
132
- {
133
- name: 'unauthenticated-rejected',
134
- auth: 'none',
135
- timeout: 15000,
136
-
137
- async run({ http, assert, config }) {
138
- const response = await http.post('admin/email', {
139
- subject: 'Test Email',
140
- to: [{ email: `_test-receiver@${config.domain}` }],
141
- });
142
-
143
- assert.isError(response, 401, 'Send email should fail without authentication');
144
- },
145
- },
146
-
147
- // Test 6: Non-admin user fails
148
- {
149
- name: 'non-admin-rejected',
150
- auth: 'basic',
151
- timeout: 15000,
152
-
153
- async run({ http, assert, config }) {
154
- const response = await http.post('admin/email', {
155
- subject: 'Test Email',
156
- to: [{ email: `_test-receiver@${config.domain}` }],
157
- });
158
-
159
- assert.isError(response, 403, 'Send email should fail for non-admin user');
160
- },
161
- },
162
113
  ],
163
114
  };
@@ -1,76 +0,0 @@
1
- We need to do a pretty significatn refactor of our BEM API now.
2
-
3
- Since that was orignally implemented, I built a much better process for hading incoming http requests. That is, the route/schema system found here:
4
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/helpers/assistant.js
5
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/helpers/middleware.js
6
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/helpers/settings.js
7
-
8
- You can see an example of it in our consuming project:
9
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/functions/index.js
10
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/functions/routes/example/index.js
11
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/functions/schemas/example/index.js
12
-
13
- We built a single unified bem_api function that handles all http requests in a single place, and then routes them, see here:
14
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/index.js
15
- .https.onRequest(async (req, res) => self._process((new (require(`${core}/actions/api.js`))()).init(self, { req: req, res: res, })));
16
-
17
- We should refactor this system to USE THE NEW ROUTE/SCHEMA SYSTEM rather than the old way of doing things. This will make it much easier to maintain and extend in the future.
18
-
19
- We can start with a simple one like:
20
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/functions/core/actions/api/general/generate-uuid.js
21
-
22
- and once we perfect that we can move on to the others.
23
-
24
- For each BEM refactor, we should create a route and a schema for the expected input. As you can see, BEM APIs epect a command and payload in the body, requiring it to be a POST operation. I would like to rebuild this system to be more proper, so that each BEM api can be GET, POST, etc as appropriate.
25
-
26
- Previously:
27
- request('/backend-manager', {
28
- method: 'POST',
29
- body: {
30
- command: 'generate-uuid',
31
- payload: { ... }
32
- }
33
- })
34
-
35
- but i think it owud be more intuitive if going forward we just had endpoints like:
36
- request('/backend-manager/general:uuid', {
37
- method: 'POST',
38
- body: { ... }
39
- })
40
- OR
41
- request('/backend-manager/general/uuid', {
42
- method: 'POST',
43
- body: { ... }
44
- })
45
-
46
- Im not sure how we can do this to be backwards compatible with existing BEM API consumers, but it does need to be backwards compatible.
47
-
48
- If you look at the firebase.json in the consuming project we can see that
49
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/firebase.json
50
- {
51
- "source": "/backend-manager",
52
- "function": "bm_api"
53
- },
54
-
55
- So maybe we could make it:
56
- {
57
- "source": "/backend-manager/**",
58
- "function": "bm_api"
59
- },
60
- and then parse the route inside the bem_api function to determine which route/schema to use, falling back to the old system if the route is just /backend-manager with a "command" in the body, and then use the new route/schema system if the path is /backend-manager/something?
61
-
62
- Either way, i think we need a minimal intermediary step where we determine which one to use based on the incoming request and then either just route to the old one or route to the new "middleware", "settings", route/schema system
63
-
64
- I would like each new route to have a great name clearly indicating its purpose, the method should be appropriate for the action (GET for fetches, POST for creates, etc) and the schema should be well defined for each route.
65
-
66
- Since we can build this new API system however we want, i also expect you to rewrite and refactor the BEM api endppints to be kickass, modern, and well designed.
67
-
68
- Also, certain VERBS should be removed from the actual file/function names since they are implied by the HTTP method. For example, instead of having a generate-uuid.js file, we could just have uuid.js since the POST method implies that we are generating/creating a new one, or insetad of add-marketing-contact we could just have marketing-contact.js since the POST method implies adding a new one and GET would imply fetching them.
69
-
70
- Next, some fucntions have a lot crammed isnide them that could use some separation. For example, in add-marketing-contact.js we have code for handling multiple email providers (SendGrid, Beehiiv) all jammed into a single file. I think we should refactor this to have a separate file for each provider in subfolder that handles the specifics of that provider, and then the main route file just calls those provider-specific files as needed. This will make it much easier to maintain and extend in the future as we add more providers.
71
-
72
- Also, sometimes there are two endpoints that should be combined,for example
73
- * /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/functions/core/actions/api/general/add-marketing-contact.js
74
- * /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/functions/core/actions/api/general/remove-marketing-contact.js
75
- These could be combined into a single marketing-contact.js file that handles both adding and removing based on the HTTP method (POST for add, DELETE for remove). This will make the API more RESTful and easier to understand.
76
-
@@ -1,62 +0,0 @@
1
- MIDDLEWARE REFACTOR
2
- We have a system where we handle incoming requests using a route/schema system found here:
3
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/helpers/assistant.js
4
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/helpers/middleware.js
5
- /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/helpers/settings.js
6
-
7
- You can see an example of it in our consuming project:
8
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/functions/index.js
9
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/functions/routes/example/index.js
10
- /Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-backend/functions/schemas/example/index.js
11
-
12
- I have some ideas iw as thinking about and id like to know your thoughts:
13
- * new design so that each route is modern JS that does a single export instead of exportting a class with an init() and a main() method.
14
- * schema system currently uses the user's plan when designing the schma like
15
- module.exports = function (assistant) {
16
- return {
17
- // DEFAULTS
18
- ['defaults']: {
19
- key: {
20
- types: ['string'],
21
- value: undefined,
22
- default: '',
23
- required: false,
24
- min: 0,
25
- max: 2048,
26
- },
27
- },
28
-
29
- // Premium plan
30
- ['premium']: {
31
- key: {
32
- types: ['string'],
33
- value: undefined,
34
- default: 'premium-default',
35
- required: false,
36
- min: 0,
37
- max: 4096,
38
- },
39
- }
40
- };
41
- }
42
-
43
- however it hink we should instead eliminate the toplevel plan/default system and code each plan changes into the individual keys like:
44
- const schema = {
45
- id: {
46
- types: ['string'],
47
- value: () => assistant.Manager.Utilities().randomId(),
48
- required: false,
49
- },
50
- feature: {
51
- types: ['string'],
52
- default: '',
53
- required: true,
54
- min: 1,
55
- max: 4,
56
- },
57
- };
58
-
59
- // Adjust schema based on plan
60
- if (assistant.user.plan === 'premium') {
61
- schema.feature.max = 8;
62
- }
@@ -1,66 +0,0 @@
1
- PAYMETN SYSTEM REFACTOR
2
- Question
3
- Propose a schema for storing user's subscription data in their firestore user document. Currently the user looks like this
4
- users/{uid}
5
- {
6
- auth: {
7
- uid: string,
8
- email: string,
9
- },
10
- roles: {
11
- admin: boolean,
12
- },
13
- ... (other stuff that is not really relevant)
14
- }
15
- I will be using a variety of payment processors including Stripe and PayPal, so I need a flexible way to store subscription data that can work with multiple providers.
16
- Before inserting the subscription data, we wil need to receive the processor webhooks.
17
- We will be keeping track of the subcsription data in the user doc AS WELL AS a dedicated subscription doc where we can have more information
18
- Thus, the user doc doesnt need to contain the entire subscription Object, just enough to grant the user access to premium features in the backend or frontend as well as display important info in the user's account page like next billing date, plan name, status, anything else that is important.
19
- Also, since we will be checking subscription status often, I thought it would be nice to store 2 main objects:
20
- 1. the original subscirpiton object from the payment provider, unmodified
21
- 2. a unified and standardized subscription object that we define, so that when checking subscription status we dont have to deal with the differences between payment providers (both for displaying info and for checking status/plan/etc to grant access when making requests or on the frontend)
22
- We could store BOTH in both the user doc and the subscription doc, or only store the unified object in the user doc and both in the subscription doc.
23
-
24
- For our standardized Object, we should be able to get the current plan and whether the subscription is active or not EXTREMELY easily. LIke a single if statement, allowing us to grant access if if the user is premium or whatever.
25
-
26
- However, i would like it to be flexible enough so that we can show something like:
27
- - User is on premium plan and paid up --> grant access to premium features, show "You are Premium and your next billing date is X"
28
- - User is on premium plan but payment failed --> restrict access to premium features, show "Your payment failed, please update your payment method"
29
- - User was on premium plan but cancelled --> restrict access to premium features, show "You WERE premium but it was cancelled so now youre on Free plan"
30
- - User was on premium plan but cancellation is pending --> grant access to premium features, show "You are Premium until X date when your plan will be cancelled"
31
- - User is on trial --> grant access to premium features, show "You are on a free trial that ends on X date"
32
- So essentially we need a way to be able to determine all of these different scenarios EASILY (SINGLE IF STATEMENT)
33
-
34
- I know all payment proivders are different and ahve different concepts of how exaclty a subscirption is active, cancelled, past due, trialing, etc but we need to come up with a unified way of representing this data in our standardized object so that we can easily check status and display info regardless of payment provider.
35
-
36
- Like, i i think stripe you can "cancel at period end" and thus the sub will not actually be cancelled until the end of the billing period, but paypal might be slightly different.
37
-
38
- BEM API ENDPOINTS
39
- * For our payment system to work, we shoudl implemnt some BEM API endpoints to create, listen for webhooks, and manage subscriptions.
40
- * anytime there is an aciton that handles multiple payment providers, we should have the entryppoint import a file for each provider, where each provider handles the request in its own way, but STILL STANDARDIZED and similar across all providers.
41
-
42
- backend-manager/payments/intent
43
- * handle creating payment intents or equivalent in other providers
44
- * various checks like
45
- * is the user currently subscribed? if so block
46
- * is the user allowed to have a trial (havent had one before)? if not block their trial
47
- * validate their gcaptcha token. block if invalid or missing
48
- * create the payment intent or equivalent at "payments-intents/{id}" in firestore
49
-
50
- backend-manager/payments/webhook (or something similar)
51
- * handle receiving webhooks from payment providers to update subscription status
52
- * different file for processing each paymnt processor (returning some comon things like the event id)
53
- * various checks like
54
- * verify the webhook by checking the querystring for the BEM token (same as .env BACKEND_MANAGER_KEY)
55
- * check for payment-webhooks/{id} doc to see if we already processed this webhook (id is provided by the payment provider in the webhook payload)
56
- * Immediately save the raw webhook data to "payments-webhooks/{id}" in firestore so we can return a 200 as soon as possible. THe webhook will be processed in a firestore function trigger onWrite for that doc (status === pending)
57
-
58
- Firestore trigger for payments-webhooks/{id}
59
- * process the webhook data and update the user's subscription data in both their user doc and their subscription doc
60
- * various checks like
61
- * if status === completed, do nothing
62
-
63
- THen, we need to plan an effective way to test all of these scenarios in our emualted testing environment which you can explore here: /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/test
64
- * we should be able to START with certain subscripton levels (basic/free, premium etc) and then see how events influence and change the subscription status and data in the user doc, subscription doc, webhook event doc, etc
65
-
66
- So webhook comes in --> save immediateyl to return 200 to the payment provider --> process the webhook in a separate function trigger to update subscription data and user access (reprocess if something goes wrong, etc)
File without changes