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
package/CHANGELOG.md CHANGED
@@ -33,7 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
33
33
  - Payment schemas for webhook and intent validation.
34
34
  - `payment.processors` config section for Stripe, PayPal, Chargebee, and Coinbase configuration.
35
35
  - `npx bm stripe` CLI command for standalone Stripe webhook forwarding.
36
- - Auto-start Stripe CLI webhook forwarding with `npx bm emulators` (gracefully skips when prerequisites are missing).
36
+ - Auto-start Stripe CLI webhook forwarding with `npx bm emulator` (gracefully skips when prerequisites are missing).
37
37
  - `Manager.version` property exposing the BEM package version.
38
38
  - Journey test accounts for payment lifecycle testing (upgrade, cancel, suspend, trial).
39
39
  - Stripe fixture data for subscription states (active, trialing, canceled).
@@ -57,7 +57,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
57
57
  - Firestore security rules testing support.
58
58
  - HTTP client with auth helpers (`http.as('admin').command()`).
59
59
  - Rich assertion library (`isSuccess`, `isError`, `hasProperty`, etc.).
60
- - New `bm emulators` command for standalone emulator management.
60
+ - New `bm emulator` command for standalone emulator management.
61
61
  - Enhanced `bm test` with path filtering and parallel test support.
62
62
 
63
63
  ### Changed
package/CLAUDE.md CHANGED
@@ -51,13 +51,19 @@ src/
51
51
  index.js # Main Manager class
52
52
  helpers/ # Helper classes
53
53
  assistant.js # Request/response handling
54
- user.js # User property structure
54
+ user.js # User property structure + schema
55
55
  analytics.js # GA4 integration
56
56
  usage.js # Rate limiting
57
57
  middleware.js # Request pipeline
58
58
  settings.js # Schema validation
59
59
  utilities.js # Batch operations
60
60
  metadata.js # Timestamps/tags
61
+ libraries/
62
+ payment-processors/ # Shared payment processor utilities
63
+ stripe.js # Stripe SDK init, fetchResource, toUnified*
64
+ test.js # Test processor (delegates to Stripe shapes)
65
+ order-id.js # Order ID generation (XXXX-XXXX-XXXX)
66
+ resolve-price-id.js # Shared price ID resolver from config
61
67
  functions/core/ # Built-in functions
62
68
  actions/
63
69
  api.js # Main bm_api handler
@@ -65,14 +71,35 @@ src/
65
71
  events/
66
72
  auth/ # Auth event handlers
67
73
  firestore/ # Firestore triggers
74
+ payments-webhooks/ # Webhook processing pipeline
75
+ on-write.js # Orchestrator: fetch→transform→transition→write
76
+ analytics.js # Payment analytics tracking (GA4, Meta, TikTok)
77
+ transitions/ # State transition detection + handlers
78
+ index.js # Transition detection logic
79
+ send-email.js # Shared email helper for handlers
80
+ subscription/ # Subscription transition handlers
81
+ one-time/ # One-time payment transition handlers
68
82
  cron/
69
83
  daily.js # Daily cron runner
70
84
  daily/{job}.js # Individual cron jobs
71
85
  routes/ # Built-in routes
86
+ payments/
87
+ intent/ # POST /payments/intent
88
+ post.js # Intent creation orchestrator
89
+ processors/ # Per-processor intent creators
90
+ stripe.js # Stripe Checkout Session creation
91
+ test.js # Test processor (auto-fires webhooks)
92
+ webhook/ # POST /payments/webhook
93
+ post.js # Webhook ingestion + Firestore write
94
+ processors/ # Per-processor webhook parsers
95
+ stripe.js # Stripe event parsing + categorization
96
+ test.js # Test processor (delegates to Stripe)
72
97
  schemas/ # Built-in schemas
73
98
  cli/
74
99
  index.js # CLI entry point
75
100
  commands/ # CLI commands
101
+ test/
102
+ test-accounts.js # Test account definitions (static + journey)
76
103
  templates/
77
104
  backend-manager-config.json # Config template
78
105
  ```
@@ -387,10 +414,10 @@ Manager.handlers.bm_api = function (mod, position) {
387
414
  ### Running Tests
388
415
  ```bash
389
416
  # Option 1: Two terminals
390
- npx bm emulators # Terminal 1 - keeps emulators running
391
- npx bm test # Terminal 2 - runs tests
417
+ npx bm emulator # Terminal 1 - keeps emulator running
418
+ npx bm test # Terminal 2 - runs tests
392
419
 
393
- # Option 2: Single command (auto-starts emulators)
420
+ # Option 2: Single command (auto-starts emulator)
394
421
  npx bm test
395
422
  ```
396
423
 
@@ -492,7 +519,7 @@ assert.fail(message) // Explicit fail
492
519
 
493
520
  ## Stripe Webhook Forwarding
494
521
 
495
- BEM auto-starts Stripe CLI webhook forwarding when running `npx bm serve` or `npx bm emulators`. This forwards Stripe test webhooks to the local server so the full payment pipeline works end-to-end during development.
522
+ BEM auto-starts Stripe CLI webhook forwarding when running `npx bm serve` or `npx bm emulator`. This forwards Stripe test webhooks to the local server so the full payment pipeline works end-to-end during development.
496
523
 
497
524
  **Requirements:**
498
525
  - `STRIPE_SECRET_KEY` set in `functions/.env`
@@ -594,8 +621,10 @@ subscription: {
594
621
  },
595
622
  payment: {
596
623
  processor: null, // 'stripe' | 'paypal' | etc.
624
+ orderId: null, // BEM order ID (e.g., '1234-5678-9012')
597
625
  resourceId: null, // provider subscription ID (e.g., 'sub_xxx')
598
626
  frequency: null, // 'monthly' | 'annually'
627
+ price: 0, // resolved from config (number, e.g., 4.99)
599
628
  startDate: { timestamp, timestampUNIX },
600
629
  updatedBy: {
601
630
  event: { name: null, id: null },
@@ -638,13 +667,14 @@ The `transitions/index.js` module compares the **before** state (current `users/
638
667
  | Transition | Before → After | File |
639
668
  |---|---|---|
640
669
  | `new-subscription` | basic/null → active paid | `transitions/subscription/new-subscription.js` |
641
- | `trial-started` | basic/null → active paid with trial | `transitions/subscription/trial-started.js` |
642
670
  | `payment-failed` | active → suspended | `transitions/subscription/payment-failed.js` |
643
671
  | `payment-recovered` | suspended → active | `transitions/subscription/payment-recovered.js` |
644
672
  | `cancellation-requested` | pending=false → pending=true | `transitions/subscription/cancellation-requested.js` |
645
673
  | `subscription-cancelled` | non-cancelled → cancelled | `transitions/subscription/subscription-cancelled.js` |
646
674
  | `plan-changed` | active product A → active product B | `transitions/subscription/plan-changed.js` |
647
675
 
676
+ Note: Trials are NOT a separate transition. The `new-subscription` handler checks `after.trial.claimed` to determine if the subscription started with a trial.
677
+
648
678
  ### One-Time Transitions
649
679
 
650
680
  | Transition | Event Type | File |
@@ -672,6 +702,107 @@ module.exports = async function ({ before, after, uid, userDoc, admin, assistant
672
702
  2. Create handler file in `transitions/{category}/{name}.js`
673
703
  3. Handler receives full context — use `assistant.log()` for logging, `Manager.project.apiUrl` for API calls
674
704
 
705
+ ## Payment System Architecture
706
+
707
+ ### Pipeline
708
+
709
+ The payment system follows a linear pipeline: **Intent → Webhook → On-Write → Transition**.
710
+
711
+ 1. **Intent** (`POST /payments/intent`): Client requests a payment session. BEM validates the product, generates an order ID (`XXXX-XXXX-XXXX`), and delegates to the processor module (e.g., Stripe creates a Checkout Session). Saves to `payments-intents/{orderId}`.
712
+
713
+ 2. **Webhook** (`POST /payments/webhook?processor=X&key=Y`): Processor sends event data. BEM parses and categorizes the event (`subscription` or `one-time`), extracts the UID, and saves to `payments-webhooks/{eventId}` with `status: 'pending'`.
714
+
715
+ 3. **On-Write** (Firestore trigger on `payments-webhooks/{eventId}`): Fetches the latest resource from the processor API (not stale webhook data), transforms it into a unified object, detects state transitions, dispatches handlers, tracks analytics, and writes to `users/{uid}.subscription` (subscriptions) and `payments-orders/{orderId}`.
716
+
717
+ 4. **Transitions** (fire-and-forget): Handler files run asynchronously after detection. Failures never block webhook processing. Skipped during tests unless `TEST_EXTENDED_MODE` is set.
718
+
719
+ ### Processor Interface
720
+
721
+ Each processor implements two modules:
722
+
723
+ **Intent processor** (`routes/payments/intent/processors/{processor}.js`):
724
+ ```javascript
725
+ module.exports = {
726
+ async createIntent({ uid, orderId, product, productId, frequency, trial, confirmationUrl, cancelUrl, Manager, assistant }) {
727
+ return { id, url, raw };
728
+ },
729
+ };
730
+ ```
731
+
732
+ **Webhook processor** (`routes/payments/webhook/processors/{processor}.js`):
733
+ ```javascript
734
+ module.exports = {
735
+ isSupported(eventType) { return boolean; },
736
+ parseWebhook(req) { return { eventId, eventType, category, resourceType, resourceId, raw, uid }; },
737
+ };
738
+ ```
739
+
740
+ **Shared library** (`libraries/payment-processors/{processor}.js`):
741
+ ```javascript
742
+ module.exports = {
743
+ init() { /* return SDK instance */ },
744
+ async fetchResource(resourceType, resourceId, rawFallback, context) { /* return resource */ },
745
+ toUnifiedSubscription(rawSubscription, options) { /* return unified object */ },
746
+ toUnifiedOneTime(rawResource, options) { /* return unified object */ },
747
+ };
748
+ ```
749
+
750
+ ### Product Configuration
751
+
752
+ Products are defined in `backend-manager-config.json` under `payment.products`:
753
+
754
+ ```javascript
755
+ payment: {
756
+ products: [
757
+ {
758
+ id: 'basic', // Free tier (no prices)
759
+ name: 'Basic',
760
+ type: 'subscription',
761
+ limits: { requests: 100 },
762
+ },
763
+ {
764
+ id: 'premium', // Paid subscription
765
+ name: 'Premium',
766
+ type: 'subscription',
767
+ limits: { requests: 1000 },
768
+ trial: { days: 14 }, // Optional trial period
769
+ prices: {
770
+ monthly: { amount: 4.99, stripe: 'price_xxx', paypal: null },
771
+ annually: { amount: 49.99, stripe: 'price_yyy', paypal: null },
772
+ },
773
+ },
774
+ {
775
+ id: 'credits-100', // One-time purchase
776
+ name: '100 Credits',
777
+ type: 'one-time',
778
+ prices: {
779
+ once: { amount: 9.99, stripe: 'price_zzz' },
780
+ },
781
+ },
782
+ ],
783
+ }
784
+ ```
785
+
786
+ Key rules:
787
+ - `type` is `'subscription'` (default) or `'one-time'`
788
+ - Subscription prices are keyed by frequency: `monthly`, `annually`
789
+ - One-time prices are keyed as `once`
790
+ - Each price object has processor-specific IDs (`stripe`, `paypal`, etc.)
791
+ - `basic` product has no `prices` — it's the free tier
792
+
793
+ ### Firestore Collections
794
+
795
+ | Collection | Key | Purpose |
796
+ |---|---|---|
797
+ | `payments-intents/{orderId}` | Order ID | Intent metadata (processor, product, status) |
798
+ | `payments-webhooks/{eventId}` | Processor event ID | Webhook processing state + transition result |
799
+ | `payments-orders/{orderId}` | Order ID | Unified order data (single source of truth for orders) |
800
+ | `users/{uid}.subscription` | User UID | Current subscription state (subscriptions only) |
801
+
802
+ ### Test Processor
803
+
804
+ The `test` processor generates Stripe-shaped data and auto-fires webhooks to the local server. Only available in non-production environments. Use `processor: 'test'` in intent requests during testing. The test webhook processor delegates to Stripe's parser since it generates Stripe-shaped payloads.
805
+
675
806
  ## Common Mistakes to Avoid
676
807
 
677
808
  1. **Don't modify Manager internals directly** - Use factory methods and public APIs
@@ -699,14 +830,22 @@ module.exports = async function ({ before, after, uid, userDoc, admin, assistant
699
830
  | Middleware pipeline | `src/manager/helpers/middleware.js` |
700
831
  | Schema validation | `src/manager/helpers/settings.js` |
701
832
  | Rate limiting | `src/manager/helpers/usage.js` |
702
- | User properties | `src/manager/helpers/user.js` |
833
+ | User properties + schema | `src/manager/helpers/user.js` |
703
834
  | Batch utilities | `src/manager/helpers/utilities.js` |
704
835
  | Main API handler | `src/manager/functions/core/actions/api.js` |
705
836
  | Config template | `templates/backend-manager-config.json` |
706
837
  | CLI entry | `src/cli/index.js` |
707
838
  | Stripe webhook forwarding | `src/cli/commands/stripe.js` |
839
+ | Intent creation | `src/manager/routes/payments/intent/post.js` |
840
+ | Webhook ingestion | `src/manager/routes/payments/webhook/post.js` |
841
+ | Webhook processing (on-write) | `src/manager/events/firestore/payments-webhooks/on-write.js` |
842
+ | Payment analytics | `src/manager/events/firestore/payments-webhooks/analytics.js` |
843
+ | Transition detection | `src/manager/events/firestore/payments-webhooks/transitions/index.js` |
708
844
  | Payment processor libraries | `src/manager/libraries/payment-processors/` |
709
- | Payment transition handlers | `src/manager/events/firestore/payments-webhooks/transitions/` |
845
+ | Stripe library | `src/manager/libraries/payment-processors/stripe.js` |
846
+ | Price ID resolver | `src/manager/libraries/payment-processors/resolve-price-id.js` |
847
+ | Order ID generator | `src/manager/libraries/payment-processors/order-id.js` |
848
+ | Test accounts | `src/test/test-accounts.js` |
710
849
 
711
850
  ## Environment Detection
712
851
 
package/README.md CHANGED
@@ -733,7 +733,7 @@ npx backend-manager <command>
733
733
  | `bem serve` | Start local Firebase emulator |
734
734
  | `bem deploy` | Deploy functions to Firebase |
735
735
  | `bem test [paths...]` | Run integration tests |
736
- | `bem emulators` | Start Firebase emulators (keep-alive mode) |
736
+ | `bem emulator` | Start Firebase emulator (keep-alive mode) |
737
737
  | `bem stripe` | Start Stripe CLI webhook forwarding to local server |
738
738
  | `bem version`, `bem v` | Show BEM version |
739
739
  | `bem clear` | Clear cache and temp files |
@@ -749,7 +749,7 @@ Set these in your `functions/.env` file:
749
749
  | Variable | Description |
750
750
  |----------|-------------|
751
751
  | `BACKEND_MANAGER_KEY` | Admin authentication key |
752
- | `STRIPE_SECRET_KEY` | Stripe secret key (enables auto webhook forwarding in `serve`/`emulators`) |
752
+ | `STRIPE_SECRET_KEY` | Stripe secret key (enables auto webhook forwarding in `serve`/`emulator`) |
753
753
 
754
754
  ## Response Headers
755
755
 
@@ -761,16 +761,16 @@ bm-properties: {"code":200,"tag":"functionName/executionId","usage":{...},"schem
761
761
 
762
762
  ## Testing
763
763
 
764
- BEM includes an integration test framework that runs against Firebase emulators.
764
+ BEM includes an integration test framework that runs against the Firebase emulator.
765
765
 
766
766
  ### Running Tests
767
767
 
768
768
  ```bash
769
769
  # Option 1: Two terminals (recommended for development)
770
- npx bm emulators # Terminal 1 - keeps emulators running
771
- npx bm test # Terminal 2 - runs tests
770
+ npx bm emulator # Terminal 1 - keeps emulator running
771
+ npx bm test # Terminal 2 - runs tests
772
772
 
773
- # Option 2: Single command (auto-starts emulators, shuts down after)
773
+ # Option 2: Single command (auto-starts emulator, shuts down after)
774
774
  npx bm test
775
775
  ```
776
776
 
@@ -0,0 +1,3 @@
1
+ 1. Determine name via AI on signup
2
+ 2. Add to sendgrid marketig cotacts
3
+ 3. then, develop a way to SYNC them to the marketing contacts. we coould sync on payment changes (liek newly subscribed, cancelled, etc) so we can segment them??
@@ -0,0 +1,71 @@
1
+ TODO payments
2
+
3
+ would it be beneficial to... check the time on the event and the time in our datbase and skip if the time in oour database is nwer than the vent, indicatig that it is stale? or is this redundatn since we fetch the latest resource anyway?
4
+
5
+ next, we need to do certain things based on the CHANGE thats happening to the resource (subscription or one time) that is different that simply updating the user doc and the payments-subscriptions or payments-one-time collection. For example:
6
+ * if a new subscription is created, we send a welcome email and grant access to the product (but this cant happen when a user fixes their payment method from a failed payment, only when a new subscription is created)
7
+ * if a subscription is cancelled, we send a cancellation email
8
+ * if a subscription payment fails, we send a paymetn failed "please update your payment method" email
9
+ * if a subscription payment succeeds after previously failing, we send a "payment successful, your access has been restored" email
10
+
11
+ more endpoints to build
12
+ payments/cancel
13
+ * takes a subscrition id and requests to cancel at the end of the billing period (not immediately). this can only be done if the user has an non cancelled subscription.
14
+ * there should be an accompanying frontend form that asks some outboardig quetsions
15
+ * Why are you cancelling (checkboxes + textbox) with randomzed order of the checkboes
16
+ * after doing this, users should still be able to reactivate it
17
+ payments/manage
18
+ * fetches the customer portal link from the payment processor and redirects the user there
19
+ * acessible from the user's account page in biling setion
20
+ * only accessible if the user has a non cancelled subscription
21
+
22
+ Note: * for managing and cancelling, they should be under a sinlge button/dropdown called "manage subscription".
23
+ * it could have links to the 2 pages as items in the dropdown, or we could take them to a dedicated page (or expand an accordian) that has more information about managing it
24
+ * we need to make it hard to cancel, buried in some info, and make it more prominent to manage it, since we want to encourage users to manage their subscription rather than cancelling it.
25
+
26
+ payments/refund
27
+ * takes a subscription id and requests a refund.
28
+ * we can only refund the most recent payment and we can only refund if the subscription is cancelled.
29
+ * we can only refund if the most recent payment in FULL was made less than 7 days ago, otherwise we can only refund a prorated amount.
30
+ payments/reactivate
31
+ * takes a subscription id and reactivates a cancelled subscription. this can only be done if the subscription is still within its billing period, otherwise the user would have to create a new subscription.
32
+ payments/upgrade
33
+ * takes a subscription id and a new plan id and upgrades the user's subscription to the new plan. this can only be done if the user has an active subscription.
34
+
35
+
36
+ trial support:
37
+ when a peyment intent happens, we can only grant a trial if the user has never had a trial which involves checking the paymetns-subscriptions collection for the user to see if any exist. if any exist in any form, then we dont give a trial.
38
+ we also need a test for this
39
+
40
+ block multipl epayments
41
+ check if user has a non-cancelled sub during payment intent creation, if so, block the payment intent from being created. this will prevent multiple payments from being made at the same time and causing issues. we can check the payments-subscriptions collection for ALL usbcriptions belonging to the UID. we can use this for the trial check as well (recycle the results).
42
+ we also need a test for this
43
+
44
+
45
+
46
+ also, can you check and confirm if the usage.js sends back the users current usage in th headers? i think it does? maybe it our tests we can check to ensure that the user is probably having their usage set this way?
47
+
48
+
49
+ MANAGEMENT LINK
50
+ we need an endpoint that returns a management link for the user to manage their subscription. this will involve us looking up the users current subscription, then calling the appropriate method on the payment processor to get a management link. we can only return a management link if the user has an active subscription.
51
+
52
+
53
+ TODO
54
+
55
+ * authorizations ystem that tries to charge the card to see if its valid?
56
+ * bm_cronDaily task that doublechecks subscriptions??
57
+ * could check existing ones eveyr month to ensure they are still vlaud and not messed up from failed webhook processing or could check every day to ensure theres no users with multiple subscirptions active simultaneously?
58
+ * bm_cronDaily yto handle disputes?
59
+ * automatically provide evidence?
60
+
61
+ ATTRIBUTIONS!!!
62
+ // Filter attribution entries older than 30 days
63
+ const attribution = webManager.storage().get('attribution', {});
64
+ const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days in ms
65
+ const filtered = {};
66
+
67
+ for (const [key, entry] of Object.entries(attribution)) {
68
+ if (!entry?.timestamp) continue;
69
+ if ((Date.now() - new Date(entry.timestamp).getTime()) < maxAge) {
70
+ filtered[key] = entry;
71
+ }
package/TODO.md CHANGED
@@ -23,11 +23,18 @@ BEM
23
23
  * Teach it how to mock requests and use test user's SECRET API KEYS to authenticate requests
24
24
  * BEM should create a few test accounts: basic, then one for each plan level
25
25
 
26
+ TODO
27
+ Update deps!!!! theres lots
28
+ * we culd have a test that TRIES EACH IMPORT ?? incase updating it fails due to ESM VULLSHIT?
29
+
26
30
 
27
31
  ADD HEALTHCHECK TO BEM!!!
28
32
  ✗ https://api.clockii.com/backend-manager?command=healthcheck → fetch failed
29
33
 
30
34
  TODO
35
+ PAYMENT
36
+ https://hookdeck.com/webhooks/platforms/guide-to-paypal-webhooks-features-and-best-practices
37
+
31
38
 
32
39
  # TEST REWRRK
33
40
  btw... the account.json needs to be removed. remove it from BEM and the consumong project. when we make our test system, we DO NOT NEED TO STORE THE ACCOUBT IN A JSON file. we just need a source of truth in BEM for what uid/emails to look for
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.89",
3
+ "version": "5.0.92",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
7
- "bem": "./bin/bem",
8
- "bm": "./bin/bem"
7
+ "bm": "./bin/backend-manager",
8
+ "bem": "./bin/backend-manager",
9
+ "backend-manager": "./bin/backend-manager",
10
+ "mgr": "./bin/backend-manager"
9
11
  },
10
12
  "scripts": {
11
13
  "start": "node src/manager/index.js",
@@ -18,7 +20,7 @@
18
20
  "projectScripts": {
19
21
  "start": "npx bm setup && npx bm serve",
20
22
  "deploy": "npx bm setup && npx bm deploy",
21
- "emulators": "npx bm setup && npx bm emulators",
23
+ "emulator": "npx bm setup && npx bm emulator",
22
24
  "test": "npx bm setup && npx bm test",
23
25
  "setup": "npx bm setup"
24
26
  },
@@ -47,7 +49,7 @@
47
49
  "@google-cloud/pubsub": "^5.3.0",
48
50
  "@google-cloud/storage": "^7.19.0",
49
51
  "@octokit/rest": "^19.0.13",
50
- "@sendgrid/mail": "^7.7.0",
52
+ "@sendgrid/mail": "^8.1.6",
51
53
  "@sentry/node": "^6.19.7",
52
54
  "body-parser": "^1.20.4",
53
55
  "busboy": "^1.6.0",
@@ -5,12 +5,12 @@ const jetpack = require('fs-jetpack');
5
5
  const JSON5 = require('json5');
6
6
  const powertools = require('node-powertools');
7
7
  const WatchCommand = require('./watch');
8
- const { DEFAULT_EMULATOR_PORTS } = require('./setup-tests/emulators-config');
8
+ const { DEFAULT_EMULATOR_PORTS } = require('./setup-tests/emulator-config');
9
9
 
10
- class EmulatorsCommand extends BaseCommand {
10
+ class EmulatorCommand extends BaseCommand {
11
11
  async execute() {
12
- this.log(chalk.cyan('\n Starting Firebase emulators (keep-alive mode)...\n'));
13
- this.log(chalk.gray(' Emulators will stay running until you press Ctrl+C\n'));
12
+ this.log(chalk.cyan('\n Starting Firebase emulator (keep-alive mode)...\n'));
13
+ this.log(chalk.gray(' Emulator will stay running until you press Ctrl+C\n'));
14
14
 
15
15
  // Warn if TEST_EXTENDED_MODE is enabled
16
16
  if (process.env.TEST_EXTENDED_MODE) {
@@ -26,29 +26,29 @@ class EmulatorsCommand extends BaseCommand {
26
26
  // Start Stripe webhook forwarding in background
27
27
  this.startStripeWebhookForwarding();
28
28
 
29
- // Run emulators with keep-alive command (use single quotes since runWithEmulators wraps in double quotes)
30
- const keepAliveCommand = "echo ''; echo 'Emulators ready. Press Ctrl+C to shut down...'; sleep 86400";
29
+ // Run emulator with keep-alive command (use single quotes since runWithEmulator wraps in double quotes)
30
+ const keepAliveCommand = "echo ''; echo 'Emulator ready. Press Ctrl+C to shut down...'; sleep 86400";
31
31
 
32
32
  try {
33
- await this.runWithEmulators(keepAliveCommand);
33
+ await this.runWithEmulator(keepAliveCommand);
34
34
  } catch (error) {
35
35
  // User pressed Ctrl+C - this is expected
36
- this.log(chalk.gray('\n Emulators stopped.\n'));
36
+ this.log(chalk.gray('\n Emulator stopped.\n'));
37
37
  }
38
38
  }
39
39
 
40
40
  /**
41
- * Run a command with Firebase emulators
42
- * @param {string} command - The command to execute inside emulators:exec
41
+ * Run a command with Firebase emulator
42
+ * @param {string} command - The command to execute inside the Firebase emulator
43
43
  * @returns {Promise<void>}
44
44
  */
45
- async runWithEmulators(command) {
45
+ async runWithEmulator(command) {
46
46
  const projectDir = this.main.firebaseProjectPath;
47
47
 
48
48
  // Load emulator ports from firebase.json
49
49
  const emulatorPorts = this.loadEmulatorPorts(projectDir);
50
50
 
51
- // Check for port conflicts before starting emulators
51
+ // Check for port conflicts before starting emulator
52
52
  const canProceed = await this.checkAndKillBlockingProcesses(emulatorPorts);
53
53
  if (!canProceed) {
54
54
  throw new Error('Port conflicts could not be resolved');
@@ -58,9 +58,9 @@ class EmulatorsCommand extends BaseCommand {
58
58
  // hosting is included so localhost:5002 rewrites work (e.g., /backend-manager -> bm_api)
59
59
  // pubsub is included so scheduled functions (bm_cronDaily) can be triggered in tests
60
60
  // Use double quotes for command wrapper since the command may contain single quotes (JSON strings)
61
- const emulatorsCommand = `BEM_TESTING=true firebase emulators:exec --only functions,firestore,auth,database,hosting,pubsub --ui "${command}"`;
61
+ const emulatorCommand = `BEM_TESTING=true firebase emulators:exec --only functions,firestore,auth,database,hosting,pubsub --ui "${command}"`;
62
62
 
63
- await powertools.execute(emulatorsCommand, {
63
+ await powertools.execute(emulatorCommand, {
64
64
  log: true,
65
65
  cwd: projectDir,
66
66
  });
@@ -90,4 +90,4 @@ class EmulatorsCommand extends BaseCommand {
90
90
  }
91
91
  }
92
92
 
93
- module.exports = EmulatorsCommand;
93
+ module.exports = EmulatorCommand;
@@ -8,7 +8,7 @@ module.exports = {
8
8
  ServeCommand: require('./serve'),
9
9
  DeployCommand: require('./deploy'),
10
10
  TestCommand: require('./test'),
11
- EmulatorsCommand: require('./emulators'),
11
+ EmulatorCommand: require('./emulator'),
12
12
  CleanCommand: require('./clean'),
13
13
  IndexesCommand: require('./indexes'),
14
14
  WatchCommand: require('./watch'),
@@ -25,9 +25,9 @@ const REQUIRED_EMULATORS = {
25
25
  ui: { enabled: true, port: DEFAULT_EMULATOR_PORTS.ui },
26
26
  };
27
27
 
28
- class EmulatorsConfigTest extends BaseTest {
28
+ class EmulatorConfigTest extends BaseTest {
29
29
  getName() {
30
- return 'emulators config in firebase.json';
30
+ return 'emulator config in firebase.json';
31
31
  }
32
32
 
33
33
  async run() {
@@ -75,5 +75,5 @@ class EmulatorsConfigTest extends BaseTest {
75
75
  }
76
76
  }
77
77
 
78
- module.exports = EmulatorsConfigTest;
79
- module.exports.DEFAULT_EMULATOR_PORTS = DEFAULT_EMULATOR_PORTS;
78
+ module.exports = EmulatorConfigTest;
79
+ module.exports.DEFAULT_EMULATOR_PORTS = DEFAULT_EMULATOR_PORTS;
@@ -24,7 +24,7 @@ const FirestoreIndexesInJsonTest = require('./firestore-indexes-in-json');
24
24
  const RealtimeRulesInJsonTest = require('./realtime-rules-in-json');
25
25
  const StorageRulesInJsonTest = require('./storage-rules-in-json');
26
26
  const RemoteconfigTemplateInJsonTest = require('./remoteconfig-template-in-json');
27
- const EmulatorsConfigTest = require('./emulators-config');
27
+ const EmulatorConfigTest = require('./emulator-config');
28
28
  const HostingRewritesTest = require('./hosting-rewrites');
29
29
  const FirestoreIndexesSyncedTest = require('./firestore-indexes-synced');
30
30
  const StorageLifecyclePolicyTest = require('./storage-lifecycle-policy');
@@ -64,7 +64,7 @@ function getTests(context) {
64
64
  new RealtimeRulesInJsonTest(context),
65
65
  new StorageRulesInJsonTest(context),
66
66
  new RemoteconfigTemplateInJsonTest(context),
67
- new EmulatorsConfigTest(context),
67
+ new EmulatorConfigTest(context),
68
68
  new HostingRewritesTest(context),
69
69
  new FirestoreIndexesSyncedTest(context),
70
70
  new StorageLifecyclePolicyTest(context),
@@ -9,7 +9,7 @@ const chalk = require('chalk');
9
9
  * - functions/backend-manager-config.json (firebaseConfig.projectId)
10
10
  * - functions/service-account.json (project_id)
11
11
  *
12
- * Mismatches cause tests to fail when running emulators in separate terminals
12
+ * Mismatches cause tests to fail when running emulator in separate terminals
13
13
  * because different parts of the system connect to different Firestore databases.
14
14
  */
15
15
  class ProjectIdConsistencyTest extends BaseTest {
@@ -4,8 +4,8 @@ const chalk = require('chalk');
4
4
  const jetpack = require('fs-jetpack');
5
5
  const JSON5 = require('json5');
6
6
  const powertools = require('node-powertools');
7
- const { DEFAULT_EMULATOR_PORTS } = require('./setup-tests/emulators-config');
8
- const EmulatorsCommand = require('./emulators');
7
+ const { DEFAULT_EMULATOR_PORTS } = require('./setup-tests/emulator-config');
8
+ const EmulatorCommand = require('./emulator');
9
9
 
10
10
  class TestCommand extends BaseCommand {
11
11
  async execute() {
@@ -42,14 +42,14 @@ class TestCommand extends BaseCommand {
42
42
  // Build the test command
43
43
  const testCommand = this.buildTestCommand(testConfig);
44
44
 
45
- // Check if emulators are already running
46
- const emulatorsRunning = this.areEmulatorsRunning(emulatorPorts);
45
+ // Check if emulator is already running
46
+ const emulatorRunning = this.isEmulatorRunning(emulatorPorts);
47
47
 
48
- if (emulatorsRunning) {
49
- this.log(chalk.cyan('Running tests against EXISTING emulators'));
48
+ if (emulatorRunning) {
49
+ this.log(chalk.cyan('Running tests against EXISTING emulator'));
50
50
  await this.runTestsDirectly(testCommand, functionsDir, emulatorPorts);
51
51
  } else {
52
- this.log(chalk.cyan('Starting emulators and running tests...'));
52
+ this.log(chalk.cyan('Starting emulator and running tests...'));
53
53
  await this.runEmulatorTests(testCommand);
54
54
  }
55
55
  }
@@ -164,16 +164,16 @@ class TestCommand extends BaseCommand {
164
164
  }
165
165
 
166
166
  /**
167
- * Check if emulators are already running
167
+ * Check if emulator is already running
168
168
  */
169
- areEmulatorsRunning(emulatorPorts) {
169
+ isEmulatorRunning(emulatorPorts) {
170
170
  // Check if functions emulator port is in use
171
- // If it is, assume all emulators are running
171
+ // If it is, assume emulator is running
172
172
  return this.isPortInUse(emulatorPorts.functions);
173
173
  }
174
174
 
175
175
  /**
176
- * Run tests directly (emulators already running)
176
+ * Run tests directly (emulator already running)
177
177
  */
178
178
  async runTestsDirectly(testCommand, functionsDir, emulatorPorts) {
179
179
  this.log(chalk.gray(` Hosting: http://127.0.0.1:${emulatorPorts.hosting}`));
@@ -192,16 +192,16 @@ class TestCommand extends BaseCommand {
192
192
  }
193
193
 
194
194
  /**
195
- * Run tests with Firebase emulators (starts emulators, runs tests, shuts down)
195
+ * Run tests with Firebase emulator (starts emulator, runs tests, shuts down)
196
196
  */
197
197
  async runEmulatorTests(testCommand) {
198
- this.log(chalk.gray(' Starting Firebase emulators...\n'));
198
+ this.log(chalk.gray(' Starting Firebase emulator...\n'));
199
199
 
200
- // Use EmulatorsCommand to run tests with emulators
201
- const emulatorsCmd = new EmulatorsCommand(this.main);
200
+ // Use EmulatorCommand to run tests with emulator
201
+ const emulatorCmd = new EmulatorCommand(this.main);
202
202
 
203
203
  try {
204
- await emulatorsCmd.runWithEmulators(testCommand);
204
+ await emulatorCmd.runWithEmulator(testCommand);
205
205
  } catch (error) {
206
206
  // Only exit with error if it wasn't a user-initiated exit
207
207
  if (error.code !== 0) {