backend-manager 5.0.89 → 5.0.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/CLAUDE.md +147 -8
- package/README.md +6 -6
- package/TODO-MARKETING.md +3 -0
- package/TODO-PAYMENT-v2.md +71 -0
- package/TODO.md +7 -0
- package/package.json +7 -5
- package/src/cli/commands/{emulators.js → emulator.js} +15 -15
- package/src/cli/commands/index.js +1 -1
- package/src/cli/commands/setup-tests/{emulators-config.js → emulator-config.js} +4 -4
- package/src/cli/commands/setup-tests/index.js +2 -2
- package/src/cli/commands/setup-tests/project-id-consistency.js +1 -1
- package/src/cli/commands/test.js +16 -16
- package/src/cli/index.js +15 -4
- package/src/manager/events/auth/on-create.js +5 -158
- package/src/manager/events/firestore/payments-webhooks/analytics.js +171 -0
- package/src/manager/events/firestore/payments-webhooks/on-write.js +95 -297
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +19 -10
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +4 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +61 -0
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +22 -9
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +21 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +18 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +18 -7
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +26 -8
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +24 -9
- package/src/manager/functions/core/actions/api/admin/send-email.js +0 -131
- package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +2 -137
- package/src/manager/functions/core/actions/api/user/sign-up.js +1 -1
- package/src/manager/helpers/user.js +1 -0
- package/src/manager/index.js +12 -0
- package/src/manager/libraries/email.js +483 -0
- package/src/manager/libraries/infer-contact.js +140 -0
- package/src/manager/libraries/payment-processors/resolve-price-id.js +19 -0
- package/src/manager/libraries/payment-processors/stripe.js +87 -48
- package/src/manager/libraries/payment-processors/test.js +4 -4
- package/src/manager/libraries/prompts/infer-contact.md +43 -0
- package/src/manager/routes/admin/backup/post.js +4 -3
- package/src/manager/routes/admin/email/post.js +11 -428
- package/src/manager/routes/admin/hook/post.js +3 -2
- package/src/manager/routes/admin/notification/post.js +14 -12
- package/src/manager/routes/admin/post/post.js +5 -6
- package/src/manager/routes/admin/post/put.js +3 -2
- package/src/manager/routes/admin/stats/get.js +19 -10
- package/src/manager/routes/general/email/post.js +8 -21
- package/src/manager/routes/marketing/contact/post.js +2 -100
- package/src/manager/routes/payments/intent/post.js +44 -2
- package/src/manager/routes/payments/intent/processors/stripe.js +10 -45
- package/src/manager/routes/payments/intent/processors/test.js +20 -25
- package/src/manager/routes/user/oauth2/_helpers.js +3 -2
- package/src/manager/routes/user/oauth2/delete.js +3 -3
- package/src/manager/routes/user/oauth2/get.js +2 -2
- package/src/manager/routes/user/oauth2/post.js +9 -9
- package/src/manager/routes/user/sessions/delete.js +4 -3
- package/src/manager/routes/user/signup/post.js +254 -54
- package/src/manager/schemas/admin/email/post.js +10 -5
- package/src/test/run-tests.js +1 -1
- package/src/test/runner.js +11 -0
- package/src/test/test-accounts.js +18 -0
- package/templates/backend-manager-config.json +31 -12
- package/test/events/payments/journey-payments-one-time-failure.js +105 -0
- package/test/events/payments/journey-payments-one-time.js +128 -0
- package/test/events/payments/journey-payments-plan-change.js +126 -0
- package/test/events/payments/journey-payments-upgrade.js +2 -2
- package/test/functions/admin/send-email.js +1 -88
- package/test/helpers/email.js +381 -0
- package/test/helpers/infer-contact.js +299 -0
- package/test/routes/admin/email.js +41 -90
- package/REFACTOR-BEM-API.md +0 -76
- package/REFACTOR-MIDDLEWARE.md +0 -62
- package/REFACTOR-PAYMENT.md +0 -66
- /package/bin/{bem → backend-manager} +0 -0
package/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
|
|
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
|
|
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
|
|
391
|
-
npx bm test
|
|
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
|
|
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
|
|
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
|
-
|
|
|
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
|
|
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`/`
|
|
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
|
|
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
|
|
771
|
-
npx bm test
|
|
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
|
|
773
|
+
# Option 2: Single command (auto-starts emulator, shuts down after)
|
|
774
774
|
npx bm test
|
|
775
775
|
```
|
|
776
776
|
|
|
@@ -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.
|
|
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
|
-
"
|
|
8
|
-
"
|
|
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
|
-
"
|
|
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": "^
|
|
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/
|
|
8
|
+
const { DEFAULT_EMULATOR_PORTS } = require('./setup-tests/emulator-config');
|
|
9
9
|
|
|
10
|
-
class
|
|
10
|
+
class EmulatorCommand extends BaseCommand {
|
|
11
11
|
async execute() {
|
|
12
|
-
this.log(chalk.cyan('\n Starting Firebase
|
|
13
|
-
this.log(chalk.gray('
|
|
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
|
|
30
|
-
const keepAliveCommand = "echo ''; echo '
|
|
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.
|
|
33
|
+
await this.runWithEmulator(keepAliveCommand);
|
|
34
34
|
} catch (error) {
|
|
35
35
|
// User pressed Ctrl+C - this is expected
|
|
36
|
-
this.log(chalk.gray('\n
|
|
36
|
+
this.log(chalk.gray('\n Emulator stopped.\n'));
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Run a command with Firebase
|
|
42
|
-
* @param {string} command - The command to execute inside
|
|
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
|
|
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
|
|
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
|
|
61
|
+
const emulatorCommand = `BEM_TESTING=true firebase emulators:exec --only functions,firestore,auth,database,hosting,pubsub --ui "${command}"`;
|
|
62
62
|
|
|
63
|
-
await powertools.execute(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
28
|
+
class EmulatorConfigTest extends BaseTest {
|
|
29
29
|
getName() {
|
|
30
|
-
return '
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 {
|
package/src/cli/commands/test.js
CHANGED
|
@@ -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/
|
|
8
|
-
const
|
|
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
|
|
46
|
-
const
|
|
45
|
+
// Check if emulator is already running
|
|
46
|
+
const emulatorRunning = this.isEmulatorRunning(emulatorPorts);
|
|
47
47
|
|
|
48
|
-
if (
|
|
49
|
-
this.log(chalk.cyan('Running tests against EXISTING
|
|
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
|
|
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
|
|
167
|
+
* Check if emulator is already running
|
|
168
168
|
*/
|
|
169
|
-
|
|
169
|
+
isEmulatorRunning(emulatorPorts) {
|
|
170
170
|
// Check if functions emulator port is in use
|
|
171
|
-
// If it is, assume
|
|
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 (
|
|
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
|
|
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
|
|
198
|
+
this.log(chalk.gray(' Starting Firebase emulator...\n'));
|
|
199
199
|
|
|
200
|
-
// Use
|
|
201
|
-
const
|
|
200
|
+
// Use EmulatorCommand to run tests with emulator
|
|
201
|
+
const emulatorCmd = new EmulatorCommand(this.main);
|
|
202
202
|
|
|
203
203
|
try {
|
|
204
|
-
await
|
|
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) {
|