backend-manager 5.0.38 → 5.0.39

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 (88) hide show
  1. package/.claude/settings.local.json +3 -2
  2. package/.gitignore FROM BEM TEMPALTE +74 -0
  3. package/CLAUDE.md +103 -10
  4. package/README.md +105 -7
  5. package/REFACTOR-BEM-API.md +72 -0
  6. package/REFACTOR-MIDDLEWARE.md +62 -0
  7. package/REFACTOR-PAYMENT.md +61 -0
  8. package/package.json +10 -10
  9. package/src/cli/cli-refactored.js +14 -0
  10. package/src/cli/commands/base-command.js +102 -0
  11. package/src/cli/commands/deploy.js +0 -5
  12. package/src/cli/commands/emulators.js +69 -0
  13. package/src/cli/commands/index.js +2 -0
  14. package/src/cli/commands/serve.js +5 -7
  15. package/src/cli/commands/setup-tests/emulators-config.js +77 -0
  16. package/src/cli/commands/setup-tests/env-file.js +109 -0
  17. package/src/cli/commands/setup-tests/env-runtime-config-deprecated.js +45 -0
  18. package/src/cli/commands/setup-tests/firestore-rules-file.js +11 -5
  19. package/src/cli/commands/setup-tests/gitignore.js +99 -18
  20. package/src/cli/commands/setup-tests/helpers/merge-line-files.js +181 -0
  21. package/src/cli/commands/setup-tests/hosting-folder.js +3 -3
  22. package/src/cli/commands/setup-tests/hosting-rewrites.js +1 -2
  23. package/src/cli/commands/setup-tests/index.js +10 -8
  24. package/src/cli/commands/setup-tests/legacy-tests-cleanup.js +43 -0
  25. package/src/cli/commands/setup-tests/npm-project-scripts.js +45 -0
  26. package/src/cli/commands/setup.js +12 -10
  27. package/src/cli/commands/test.js +206 -11
  28. package/src/cli/commands/watch.js +121 -0
  29. package/src/manager/functions/core/actions/api/admin/create-post.js +1 -1
  30. package/src/manager/functions/core/actions/api/admin/database-write.js +1 -1
  31. package/src/manager/functions/core/actions/api/admin/edit-post.js +11 -2
  32. package/src/manager/functions/core/actions/api/admin/get-stats.js +9 -0
  33. package/src/manager/functions/core/actions/api/admin/send-email.js +416 -192
  34. package/src/manager/functions/core/actions/api/admin/send-notification.js +66 -8
  35. package/src/manager/functions/core/actions/api/admin/sync-users.js +2 -2
  36. package/src/manager/functions/core/actions/api/admin/write-repo-content.js +11 -3
  37. package/src/manager/functions/core/actions/api/general/fetch-post.js +10 -1
  38. package/src/manager/functions/core/actions/api/general/generate-uuid.js +1 -1
  39. package/src/manager/functions/core/actions/api/general/send-email.js +2 -2
  40. package/src/manager/functions/core/actions/api/handler/create-post.js +4 -5
  41. package/src/manager/functions/core/actions/api/special/setup-electron-manager-client.js +2 -3
  42. package/src/manager/functions/core/actions/api/test/health.js +24 -0
  43. package/src/manager/functions/core/actions/api/user/create-custom-token.js +1 -3
  44. package/src/manager/functions/core/actions/api/user/delete.js +1 -1
  45. package/src/manager/functions/core/actions/api/user/get-active-sessions.js +2 -4
  46. package/src/manager/functions/core/actions/api/user/get-subscription-info.js +7 -8
  47. package/src/manager/functions/core/actions/api/user/regenerate-api-keys.js +1 -2
  48. package/src/manager/functions/core/actions/api/user/sign-out-all-sessions.js +2 -3
  49. package/src/manager/functions/core/actions/api/user/sign-up.js +182 -57
  50. package/src/manager/functions/core/actions/api/user/submit-feedback.js +1 -2
  51. package/src/manager/functions/core/actions/api.js +4 -12
  52. package/src/manager/functions/core/actions/create-post-handler.js +8 -8
  53. package/src/manager/functions/core/actions/generate-uuid.js +1 -1
  54. package/src/manager/functions/core/actions/sign-up-handler.js +6 -6
  55. package/src/manager/functions/core/admin/create-post.js +3 -4
  56. package/src/manager/functions/core/cron/daily/ghostii-auto-publisher.js +2 -2
  57. package/src/manager/functions/core/events/auth/before-create.js +36 -81
  58. package/src/manager/functions/core/events/auth/before-signin.js +19 -12
  59. package/src/manager/functions/core/events/auth/on-create.js +145 -349
  60. package/src/manager/functions/core/events/auth/on-delete.js +109 -46
  61. package/src/manager/helpers/analytics.js +1 -1
  62. package/src/manager/helpers/api-manager.js +4 -4
  63. package/src/manager/helpers/assistant.js +7 -1
  64. package/src/manager/helpers/subscription-resolver-new.js +44 -45
  65. package/src/manager/helpers/subscription-resolver.js +44 -45
  66. package/src/manager/helpers/usage.js +1 -4
  67. package/src/manager/helpers/user.js +84 -66
  68. package/src/manager/index.js +26 -58
  69. package/src/test/run-tests.js +48 -0
  70. package/src/test/runner.js +705 -0
  71. package/src/test/test-accounts.js +396 -0
  72. package/src/test/utils/assertions.js +189 -0
  73. package/src/test/utils/firestore-rules-client.js +284 -0
  74. package/src/test/utils/http-client.js +266 -0
  75. package/templates/_.env +27 -0
  76. package/templates/_.gitignore +54 -0
  77. package/templates/firestore.rules +3 -3
  78. package/src/cli/commands/setup-tests/backend-manager-tests-file.js +0 -23
  79. package/src/cli/commands/setup-tests/env-runtime-config.js +0 -68
  80. package/src/cli/commands/setup-tests/npm-dist-script.js +0 -20
  81. package/src/cli/commands/setup-tests/npm-start-script.js +0 -20
  82. package/src/manager/functions/core/actions/api/test/create-test-accounts.js +0 -27
  83. package/src/manager/functions/core/actions/api/user/sign-up copy.js +0 -544
  84. package/src/manager/functions/core/events/auth/on-create copy.js +0 -121
  85. package/src/manager/functions/test/create-test-accounts.js +0 -144
  86. package/templates/backend-manager-tests.js +0 -153
  87. package/templates/gitignore.md +0 -4
  88. /package/{src/cli → _backup}/cli.js +0 -0
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(rg:*)"
4
+ "Bash(rg:*)",
5
+ "Bash(mkdir:*)"
5
6
  ],
6
7
  "deny": []
7
8
  }
8
- }
9
+ }
@@ -0,0 +1,74 @@
1
+ # ========== Default Values ==========
2
+ # macOS
3
+ .DS_Store
4
+
5
+ # Logs
6
+ logs
7
+ *.log
8
+ npm-debug.log*
9
+ yarn-debug.log*
10
+ yarn-error.log*
11
+ firebase-debug.log*
12
+
13
+ # Firebase cache
14
+ .firebase/
15
+
16
+ # Firebase config
17
+ # Uncomment this if you'd like others to create their own Firebase project.
18
+ # For a team working on the same Firebase project(s), it is recommended to leave
19
+ # it commented so all members can deploy to the same project(s) in .firebaserc.
20
+ .firebaserc
21
+ functions/service-account*.json
22
+
23
+ # Runtime data
24
+ pids
25
+ *.pid
26
+ *.seed
27
+ *.pid.lock
28
+
29
+ # Directory for instrumented libs generated by jscoverage/JSCover
30
+ lib-cov
31
+
32
+ # Coverage directory used by tools like istanbul
33
+ coverage
34
+
35
+ # nyc test coverage
36
+ .nyc_output
37
+
38
+ # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
39
+ .grunt
40
+
41
+ # Bower dependency directory (https://bower.io/)
42
+ bower_components
43
+
44
+ # node-waf configuration
45
+ .lock-wscript
46
+
47
+ # Compiled binary addons (http://nodejs.org/api/addons.html)
48
+ build/Release
49
+
50
+ # Dependency directories
51
+ node_modules/
52
+
53
+ # Optional npm cache directory
54
+ .npm
55
+
56
+ # Optional eslint cache
57
+ .eslintcache
58
+
59
+ # Optional REPL history
60
+ .node_repl_history
61
+
62
+ # Output of 'npm pack'
63
+ *.tgz
64
+
65
+ # Yarn Integrity file
66
+ .yarn-integrity
67
+
68
+ # dotenv environment variables file
69
+ .env
70
+
71
+
72
+ # ========== Custom Values ==========
73
+ # Add your custom ignore patterns below this line
74
+ # ...
package/CLAUDE.md CHANGED
@@ -384,19 +384,112 @@ Manager.handlers.bm_api = function (mod, position) {
384
384
 
385
385
  ## Testing
386
386
 
387
+ ### Running Tests
387
388
  ```bash
388
- # Run all tests
389
- npm test
390
-
391
- # Test files are in test/
392
- test/
393
- cli-commands.test.js
394
- usage.js
395
- user.js
396
- payment-resolver/
397
- ai/
389
+ # Option 1: Two terminals
390
+ npx bm emulators # Terminal 1 - keeps emulators running
391
+ npx bm test # Terminal 2 - runs tests
392
+
393
+ # Option 2: Single command (auto-starts emulators)
394
+ npx bm test
395
+ ```
396
+
397
+ ### Filtering Tests
398
+ ```bash
399
+ npx bm test rules/ # Run rules tests (both BEM and project)
400
+ npx bm test bem:rules/ # Only BEM's rules tests
401
+ npx bm test project:rules/ # Only project's rules tests
402
+ npx bm test user/ admin/ # Multiple paths
403
+ ```
404
+
405
+ ### Test Locations
406
+ - **BEM core tests:** `test/`
407
+ - **Project tests:** `functions/test/bem/`
408
+
409
+ Use `bem:` or `project:` prefix to filter by source.
410
+
411
+ ### Test Types
412
+
413
+ | Type | Use When | Behavior |
414
+ |------|----------|----------|
415
+ | Standalone | Single logical test | Runs once |
416
+ | Suite (`type: 'suite'`) | Sequential dependent tests | Shared state, stops on failure |
417
+ | Group (`type: 'group'`) | Multiple independent tests | Continues on failure |
418
+
419
+ ### Standalone Test
420
+ ```javascript
421
+ module.exports = {
422
+ description: 'Test name',
423
+ auth: 'none', // none, user, admin, premium-active, premium-expired
424
+ timeout: 10000,
425
+ async run({ http, assert, accounts, firestore, state, waitFor }) { },
426
+ async cleanup({ ... }) { }, // Optional
427
+ };
398
428
  ```
399
429
 
430
+ ### Suite (Sequential with Shared State)
431
+ ```javascript
432
+ module.exports = {
433
+ description: 'Suite name',
434
+ type: 'suite',
435
+ tests: [
436
+ { name: 'step-1', async run({ state }) { state.value = 'shared'; } },
437
+ { name: 'step-2', async run({ state }) { /* state.value available */ } },
438
+ ],
439
+ };
440
+ ```
441
+
442
+ ### Group (Independent Tests)
443
+ ```javascript
444
+ module.exports = {
445
+ description: 'Group name',
446
+ type: 'group',
447
+ tests: [
448
+ { name: 'test-1', auth: 'admin', async run({ http, assert }) { } },
449
+ { name: 'test-2', auth: 'none', async run({ http, assert }) { } },
450
+ ],
451
+ };
452
+ ```
453
+
454
+ ### Context Object
455
+ | Property | Description |
456
+ |----------|-------------|
457
+ | `http` | HTTP client (`http.command()`, `http.as('admin').command()`) |
458
+ | `assert` | Assertion helpers (see below) |
459
+ | `accounts` | Test accounts `{ basic, admin, premium-active, ... }` |
460
+ | `firestore` | Direct DB access (`get`, `set`, `delete`, `exists`) |
461
+ | `state` | Shared state (suites only) |
462
+ | `waitFor` | Polling helper `waitFor(condition, timeout, interval)` |
463
+
464
+ ### Assert Methods
465
+ ```javascript
466
+ assert.ok(value, message) // Truthy
467
+ assert.equal(a, b, message) // Strict equality
468
+ assert.notEqual(a, b, message) // Not equal
469
+ assert.deepEqual(a, b, message) // Deep equality
470
+ assert.match(value, /regex/, message) // Regex match
471
+ assert.isSuccess(response, message) // Response success
472
+ assert.isError(response, code, message) // Response error with code
473
+ assert.hasProperty(obj, 'path.to.prop', msg) // Property exists
474
+ assert.propertyEquals(obj, 'path', value, msg) // Property value
475
+ assert.isType(value, 'string', message) // Type check
476
+ assert.contains(array, value, message) // Array includes
477
+ assert.inRange(value, min, max, message) // Number range
478
+ assert.fail(message) // Explicit fail
479
+ ```
480
+
481
+ ### Auth Levels
482
+ `none`, `user`/`basic`, `admin`, `premium-active`, `premium-expired`
483
+
484
+ ### Key Test Files
485
+ | File | Purpose |
486
+ |------|---------|
487
+ | `src/test/runner.js` | Test runner |
488
+ | `test/` | BEM core tests |
489
+ | `src/test/utils/assertions.js` | Assert helpers |
490
+ | `src/test/utils/http-client.js` | HTTP client |
491
+ | `src/test/test-accounts.js` | Test account definitions |
492
+
400
493
  ## Common Mistakes to Avoid
401
494
 
402
495
  1. **Don't modify Manager internals directly** - Use factory methods and public APIs
package/README.md CHANGED
@@ -359,7 +359,7 @@ The main API endpoint accepts commands in the format `category:action`:
359
359
  | `general` | `generate-uuid`, `send-email`, `fetch-post` |
360
360
  | `handler` | `create-post` |
361
361
  | `firebase` | `get-providers` |
362
- | `test` | `authenticate`, `create-test-accounts`, `webhook`, `lab`, `redirect` |
362
+ | `test` | `authenticate`, `webhook`, `lab`, `redirect` |
363
363
  | `special` | `setup-electron-manager-client` |
364
364
 
365
365
  ### Auth Events
@@ -737,7 +737,8 @@ npx backend-manager <command>
737
737
  | `bem setup` | Run Firebase project setup and validation |
738
738
  | `bem serve` | Start local Firebase emulator |
739
739
  | `bem deploy` | Deploy functions to Firebase |
740
- | `bem test` | Run test suite |
740
+ | `bem test [paths...]` | Run integration tests |
741
+ | `bem emulators` | Start Firebase emulators (keep-alive mode) |
741
742
  | `bem version`, `bem v` | Show BEM version |
742
743
  | `bem clear` | Clear cache and temp files |
743
744
  | `bem install`, `bem i` | Install BEM (local or production) |
@@ -747,14 +748,11 @@ npx backend-manager <command>
747
748
 
748
749
  ## Environment Variables
749
750
 
751
+ Set these in your `functions/.env` file:
752
+
750
753
  | Variable | Description |
751
754
  |----------|-------------|
752
- | `FIREBASE_CONFIG` | Firebase project config (auto-set by Firebase) |
753
- | `RUNTIME_CONFIG` | BEM runtime config (JSON5 format) |
754
755
  | `BACKEND_MANAGER_KEY` | Admin authentication key |
755
- | `ENVIRONMENT` | `'production'` or `'development'` |
756
- | `GOOGLE_APPLICATION_CREDENTIALS` | Path to service account JSON |
757
- | `HCAPTCHA_SECRET` | hCaptcha secret for usage validation |
758
756
 
759
757
  ## Response Headers
760
758
 
@@ -764,6 +762,106 @@ BEM attaches metadata to responses:
764
762
  bm-properties: {"code":200,"tag":"functionName/executionId","usage":{...},"schema":{...}}
765
763
  ```
766
764
 
765
+ ## Testing
766
+
767
+ BEM includes an integration test framework that runs against Firebase emulators.
768
+
769
+ ### Running Tests
770
+
771
+ ```bash
772
+ # Option 1: Two terminals (recommended for development)
773
+ npx bm emulators # Terminal 1 - keeps emulators running
774
+ npx bm test # Terminal 2 - runs tests
775
+
776
+ # Option 2: Single command (auto-starts emulators, shuts down after)
777
+ npx bm test
778
+ ```
779
+
780
+ ### Filtering Tests
781
+
782
+ ```bash
783
+ npx bm test rules/ # Run rules tests (both BEM and project)
784
+ npx bm test bem:rules/ # Only BEM's rules tests
785
+ npx bm test project:rules/ # Only project's rules tests
786
+ npx bm test user/ admin/ # Multiple paths
787
+ ```
788
+
789
+ ### Test Locations
790
+
791
+ - **BEM core tests:** `test/`
792
+ - **Project tests:** `functions/test/bem/`
793
+
794
+ Use `bem:` or `project:` prefix to filter by source.
795
+
796
+ ### Writing Tests
797
+
798
+ **Suite** - Sequential tests with shared state (stops on first failure):
799
+
800
+ ```javascript
801
+ // test/functions/user/sign-up.js
802
+ module.exports = {
803
+ description: 'User signup flow with affiliate tracking',
804
+ type: 'suite',
805
+ tests: [
806
+ {
807
+ name: 'verify-referrer-exists',
808
+ async run({ firestore, assert, state, accounts }) {
809
+ state.referrerUid = accounts.referrer.uid;
810
+ const doc = await firestore.get(`users/${state.referrerUid}`);
811
+ assert.ok(doc, 'Referrer should exist');
812
+ },
813
+ },
814
+ {
815
+ name: 'call-user-signup-with-affiliate',
816
+ async run({ http, assert, state }) {
817
+ const response = await http.as('referred').command('user:sign-up', {
818
+ attribution: { affiliate: { code: 'TESTREF' } },
819
+ });
820
+ assert.isSuccess(response);
821
+ },
822
+ },
823
+ ],
824
+ };
825
+ ```
826
+
827
+ **Group** - Independent tests (continues even if one fails):
828
+
829
+ ```javascript
830
+ // test/functions/admin/firestore-write.js
831
+ module.exports = {
832
+ description: 'Admin Firestore write operation',
833
+ type: 'group',
834
+ tests: [
835
+ {
836
+ name: 'admin-auth-succeeds',
837
+ auth: 'admin',
838
+ async run({ http, assert }) {
839
+ const response = await http.command('admin:firestore-write', {
840
+ path: '_test/doc',
841
+ document: { test: 'value' },
842
+ });
843
+ assert.isSuccess(response);
844
+ },
845
+ },
846
+ {
847
+ name: 'unauthenticated-rejected',
848
+ auth: 'none',
849
+ async run({ http, assert }) {
850
+ const response = await http.command('admin:firestore-write', {
851
+ path: '_test/doc',
852
+ document: { test: 'value' },
853
+ });
854
+ assert.isError(response, 401);
855
+ },
856
+ },
857
+ ],
858
+ };
859
+ ```
860
+
861
+ **Auth levels:** `none`, `user`/`basic`, `admin`, `premium-active`, `premium-expired`
862
+
863
+ See `CLAUDE.md` for complete test API documentation.
864
+
767
865
  ## Final Words
768
866
 
769
867
  If you are still having difficulty, we would love for you to post a question to [the Backend Manager issues page](https://github.com/itw-creative-works/backend-manager/issues). It is much easier to answer questions that include your code and relevant files! So if you can provide them, we'd be extremely grateful (and more likely to help you find the answer!)
@@ -0,0 +1,72 @@
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
+
69
+
70
+
71
+
72
+
@@ -0,0 +1,62 @@
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
+ }
@@ -0,0 +1,61 @@
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.38",
3
+ "version": "5.0.39",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -8,13 +8,6 @@
8
8
  "bm": "./bin/bem"
9
9
  },
10
10
  "scripts": {
11
- "_test": "npm run prepare && ./node_modules/mocha/bin/mocha test/ --recursive --timeout=10000",
12
- "test": "./node_modules/mocha/bin/mocha test/ --recursive --timeout=10000",
13
- "test:cli": "./node_modules/mocha/bin/mocha test/cli-commands.test.js --timeout=10000",
14
- "test:usage": "./node_modules/mocha/bin/mocha test/usage.js --timeout=10000",
15
- "test:payment-resolver": "./node_modules/mocha/bin/mocha test/payment-resolver/index.js --timeout=10000",
16
- "test:user": "./node_modules/mocha/bin/mocha test/user.js --timeout=10000",
17
- "test:ai": "./node_modules/mocha/bin/mocha test/ai/index.js --timeout=10000",
18
11
  "start": "node src/manager/index.js",
19
12
  "prepare": "node -e \"require('prepare-package')()\"",
20
13
  "prepare:watch": "nodemon -w ./src -e '*' --exec 'npm run prepare'"
@@ -22,6 +15,13 @@
22
15
  "engines": {
23
16
  "node": "22"
24
17
  },
18
+ "projectScripts": {
19
+ "start": "npx bm setup && npx bm serve",
20
+ "deploy": "npx bm setup && npx bm deploy",
21
+ "emulators": "npx bm setup && npx bm emulators",
22
+ "test": "npx bm setup && npx bm test",
23
+ "setup": "npx bm setup"
24
+ },
25
25
  "repository": {
26
26
  "type": "git",
27
27
  "url": "git+https://github.com/itw-creative-works/backend-manager.git"
@@ -43,7 +43,7 @@
43
43
  "replace": {}
44
44
  },
45
45
  "dependencies": {
46
- "@firebase/rules-unit-testing": "^2.0.7",
46
+ "@firebase/rules-unit-testing": "^5.0.0",
47
47
  "@google-cloud/storage": "^7.16.0",
48
48
  "@octokit/rest": "^19.0.13",
49
49
  "@sendgrid/mail": "^7.7.0",
@@ -67,7 +67,7 @@
67
67
  "lowdb": "^1.0.0",
68
68
  "mailchimp-api-v3": "^1.15.0",
69
69
  "mime-types": "^2.1.35",
70
- "mocha": "^8.4.0",
70
+ "mocha": "^11.7.5",
71
71
  "moment": "^2.30.1",
72
72
  "nanoid": "^3.3.11",
73
73
  "node-fetch": "^2.7.0",
@@ -10,8 +10,10 @@ const InstallCommand = require('./commands/install');
10
10
  const ServeCommand = require('./commands/serve');
11
11
  const DeployCommand = require('./commands/deploy');
12
12
  const TestCommand = require('./commands/test');
13
+ const EmulatorsCommand = require('./commands/emulators');
13
14
  const CleanCommand = require('./commands/clean');
14
15
  const IndexesCommand = require('./commands/indexes');
16
+ const WatchCommand = require('./commands/watch');
15
17
 
16
18
  function Main() {}
17
19
 
@@ -92,11 +94,23 @@ Main.prototype.process = async function (args) {
92
94
  return await cmd.execute();
93
95
  }
94
96
 
97
+ // Emulators (keep-alive mode)
98
+ if (self.options['emulators'] || self.options['emulator']) {
99
+ const cmd = new EmulatorsCommand(self);
100
+ return await cmd.execute();
101
+ }
102
+
95
103
  // Clean
96
104
  if (self.options['clean:npm']) {
97
105
  const cmd = new CleanCommand(self);
98
106
  return await cmd.execute();
99
107
  }
108
+
109
+ // Watch (trigger hot reload when BEM source changes)
110
+ if (self.options['watch']) {
111
+ const cmd = new WatchCommand(self);
112
+ return await cmd.execute();
113
+ }
100
114
  };
101
115
 
102
116
  // Test method for setup command