backend-manager 5.0.88 → 5.0.91

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 (39) hide show
  1. package/CLAUDE.md +133 -2
  2. package/README.md +1 -1
  3. package/package.json +5 -3
  4. package/src/cli/index.js +11 -0
  5. package/src/manager/events/firestore/payments-webhooks/analytics.js +170 -0
  6. package/src/manager/events/firestore/payments-webhooks/on-write.js +75 -315
  7. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +20 -10
  8. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +4 -8
  9. package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +67 -0
  10. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +23 -9
  11. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +22 -8
  12. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +19 -8
  13. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +19 -7
  14. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +27 -8
  15. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +25 -9
  16. package/src/manager/helpers/user.js +2 -0
  17. package/src/manager/libraries/payment-processors/order-id.js +18 -0
  18. package/src/manager/libraries/payment-processors/resolve-price-id.js +19 -0
  19. package/src/manager/libraries/payment-processors/stripe.js +88 -47
  20. package/src/manager/libraries/payment-processors/test.js +14 -11
  21. package/src/manager/routes/payments/intent/post.js +61 -7
  22. package/src/manager/routes/payments/intent/processors/stripe.js +18 -50
  23. package/src/manager/routes/payments/intent/processors/test.js +18 -22
  24. package/src/manager/routes/payments/webhook/post.js +1 -1
  25. package/src/test/runner.js +11 -0
  26. package/src/test/test-accounts.js +20 -2
  27. package/templates/backend-manager-config.json +31 -12
  28. package/test/events/payments/journey-payments-cancel.js +2 -0
  29. package/test/events/payments/journey-payments-failure.js +2 -0
  30. package/test/events/payments/journey-payments-one-time-failure.js +105 -0
  31. package/test/events/payments/journey-payments-one-time.js +128 -0
  32. package/test/events/payments/journey-payments-plan-change.js +126 -0
  33. package/test/events/payments/journey-payments-suspend.js +2 -0
  34. package/test/events/payments/journey-payments-trial.js +4 -0
  35. package/test/events/payments/journey-payments-upgrade.js +20 -10
  36. package/test/helpers/stripe-to-unified.js +17 -0
  37. package/test/helpers/user.js +1 -0
  38. package/test/routes/payments/intent.js +10 -7
  39. /package/bin/{bem → backend-manager} +0 -0
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
  ```
@@ -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
package/README.md CHANGED
@@ -886,7 +886,7 @@ BEM includes a built-in payment/subscription system with Stripe integration (ext
886
886
 
887
887
  ### Unified Subscription Object
888
888
 
889
- The same subscription shape is stored in `users/{uid}.subscription` and `payments-subscriptions/{subId}.subscription`:
889
+ The same subscription shape is stored in `users/{uid}.subscription` and `payments-orders/{orderId}.subscription`:
890
890
 
891
891
  ```javascript
892
892
  subscription: {
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.88",
3
+ "version": "5.0.91",
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",
package/src/cli/index.js CHANGED
@@ -1,6 +1,17 @@
1
+ const os = require('os');
2
+ const path = require('path');
1
3
  const argv = require('yargs').argv;
2
4
  const _ = require('lodash');
3
5
 
6
+ // Abort if running from ~/node_modules (accidental home directory install)
7
+ const _homeDir = os.homedir();
8
+ if (__dirname.startsWith(path.join(_homeDir, 'node_modules'))) {
9
+ console.error(`\nERROR: BEM is running from ~/node_modules (home directory install).`);
10
+ console.error(`This shadows the local project copy. Fix:`);
11
+ console.error(` rm -rf ~/node_modules ~/package.json ~/package-lock.json\n`);
12
+ process.exit(1);
13
+ }
14
+
4
15
  // Import commands
5
16
  const VersionCommand = require('./commands/version');
6
17
  const ClearCommand = require('./commands/clear');
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Payment analytics tracking
3
+ * Fires server-side events for GA4, Meta Conversions API, and TikTok Events API
4
+ *
5
+ * Maps transitions to standard platform events:
6
+ * new-subscription (no trial) → purchase / Purchase / CompletePayment
7
+ * new-subscription (trial) → start_trial / StartTrial / Subscribe
8
+ * payment-recovered → purchase / Subscribe / Subscribe (recurring)
9
+ * purchase-completed → purchase / Purchase / CompletePayment
10
+ */
11
+
12
+ /**
13
+ * Track payment events across analytics platforms (non-blocking)
14
+ *
15
+ * @param {object} options
16
+ * @param {string} options.category - 'subscription' or 'one-time'
17
+ * @param {string} options.transitionName - Detected transition (e.g., 'new-subscription', 'purchase-completed')
18
+ * @param {object} options.unified - Unified subscription or one-time object
19
+ * @param {string} options.uid - User ID
20
+ * @param {string} options.processor - Payment processor (e.g., 'stripe', 'paypal')
21
+ * @param {object} options.assistant - Assistant instance
22
+ * @param {object} options.Manager - Manager instance
23
+ */
24
+ function trackPayment({ category, transitionName, unified, uid, processor, assistant, Manager }) {
25
+ try {
26
+ // Resolve the analytics event to fire based on transition
27
+ const event = resolvePaymentEvent(category, transitionName, unified, Manager.config);
28
+
29
+ if (!event) {
30
+ return;
31
+ }
32
+
33
+ assistant.log(`trackPayment: event=${event.ga4}, value=${event.value}, currency=${event.currency}, product=${event.productId}, uid=${uid}`);
34
+
35
+ // GA4 via Measurement Protocol
36
+ Manager.Analytics({ assistant, uuid: uid }).event(event.ga4, {
37
+ transaction_id: event.transactionId,
38
+ value: event.value,
39
+ currency: event.currency,
40
+ items: [{
41
+ item_id: event.productId,
42
+ item_name: event.productName,
43
+ price: event.value,
44
+ quantity: 1,
45
+ }],
46
+ payment_processor: processor,
47
+ payment_frequency: event.frequency,
48
+ is_trial: event.isTrial,
49
+ is_recurring: event.isRecurring,
50
+ });
51
+
52
+ // TODO: Meta Conversions API
53
+ // Event name: event.meta (e.g., 'Purchase', 'StartTrial', 'Subscribe')
54
+ // https://developers.facebook.com/docs/marketing-api/conversions-api
55
+
56
+ // TODO: TikTok Events API
57
+ // Event name: event.tiktok (e.g., 'CompletePayment', 'Subscribe')
58
+ // https://business-api.tiktok.com/portal/docs?id=1771100865818625
59
+ } catch (e) {
60
+ assistant.error(`trackPayment failed: ${e.message}`, e);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Resolve which analytics event to fire based on transition + unified data
66
+ * Returns null if the transition doesn't warrant an analytics event
67
+ */
68
+ function resolvePaymentEvent(category, transitionName, unified, config) {
69
+ if (category === 'subscription') {
70
+ return resolveSubscriptionEvent(transitionName, unified, config);
71
+ }
72
+
73
+ if (category === 'one-time') {
74
+ return resolveOneTimeEvent(transitionName, unified, config);
75
+ }
76
+
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Map subscription transitions to analytics events
82
+ */
83
+ function resolveSubscriptionEvent(transitionName, unified, config) {
84
+ const productId = unified.product?.id;
85
+ const productName = unified.product?.name;
86
+ const frequency = unified.payment?.frequency;
87
+ const isTrial = unified.trial?.claimed === true;
88
+ const resourceId = unified.payment?.resourceId;
89
+ const price = unified.payment?.price || 0;
90
+
91
+ if (transitionName === 'new-subscription' && isTrial) {
92
+ return {
93
+ ga4: 'start_trial',
94
+ meta: 'StartTrial',
95
+ tiktok: 'Subscribe',
96
+ value: 0,
97
+ currency: config.payment?.currency || 'USD',
98
+ productId,
99
+ productName,
100
+ frequency,
101
+ isTrial: true,
102
+ isRecurring: false,
103
+ transactionId: resourceId,
104
+ };
105
+ }
106
+
107
+ if (transitionName === 'new-subscription') {
108
+ return {
109
+ ga4: 'purchase',
110
+ meta: 'Purchase',
111
+ tiktok: 'CompletePayment',
112
+ value: price,
113
+ currency: config.payment?.currency || 'USD',
114
+ productId,
115
+ productName,
116
+ frequency,
117
+ isTrial: false,
118
+ isRecurring: false,
119
+ transactionId: resourceId,
120
+ };
121
+ }
122
+
123
+ if (transitionName === 'payment-recovered') {
124
+ return {
125
+ ga4: 'purchase',
126
+ meta: 'Subscribe',
127
+ tiktok: 'Subscribe',
128
+ value: price,
129
+ currency: config.payment?.currency || 'USD',
130
+ productId,
131
+ productName,
132
+ frequency,
133
+ isTrial: false,
134
+ isRecurring: true,
135
+ transactionId: resourceId,
136
+ };
137
+ }
138
+
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * Map one-time transitions to analytics events
144
+ */
145
+ function resolveOneTimeEvent(transitionName, unified, config) {
146
+ if (transitionName !== 'purchase-completed') {
147
+ return null;
148
+ }
149
+
150
+ const productId = unified.product?.id;
151
+ const productName = unified.product?.name;
152
+ const price = unified.payment?.price || 0;
153
+ const resourceId = unified.payment?.resourceId;
154
+
155
+ return {
156
+ ga4: 'purchase',
157
+ meta: 'Purchase',
158
+ tiktok: 'CompletePayment',
159
+ value: price,
160
+ currency: config.payment?.currency || 'USD',
161
+ productId: productId || 'unknown',
162
+ productName: productName || 'Unknown',
163
+ frequency: null,
164
+ isTrial: false,
165
+ isRecurring: false,
166
+ transactionId: resourceId,
167
+ };
168
+ }
169
+
170
+ module.exports = { trackPayment };