backend-manager 5.0.91 → 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 +14 -6
- 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 +3 -3
- 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 +4 -4
- package/src/manager/events/auth/on-create.js +5 -158
- package/src/manager/events/firestore/payments-webhooks/analytics.js +4 -3
- package/src/manager/events/firestore/payments-webhooks/on-write.js +54 -6
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +1 -2
- package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +1 -1
- package/src/manager/events/firestore/payments-webhooks/transitions/send-email.js +22 -28
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +1 -2
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +1 -2
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +1 -2
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +1 -2
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +1 -2
- package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +1 -2
- 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/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/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 +0 -2
- package/src/manager/routes/payments/intent/processors/test.js +9 -10
- 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/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/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
|
@@ -414,10 +414,10 @@ Manager.handlers.bm_api = function (mod, position) {
|
|
|
414
414
|
### Running Tests
|
|
415
415
|
```bash
|
|
416
416
|
# Option 1: Two terminals
|
|
417
|
-
npx bm
|
|
418
|
-
npx bm test
|
|
417
|
+
npx bm emulator # Terminal 1 - keeps emulator running
|
|
418
|
+
npx bm test # Terminal 2 - runs tests
|
|
419
419
|
|
|
420
|
-
# Option 2: Single command (auto-starts
|
|
420
|
+
# Option 2: Single command (auto-starts emulator)
|
|
421
421
|
npx bm test
|
|
422
422
|
```
|
|
423
423
|
|
|
@@ -519,7 +519,7 @@ assert.fail(message) // Explicit fail
|
|
|
519
519
|
|
|
520
520
|
## Stripe Webhook Forwarding
|
|
521
521
|
|
|
522
|
-
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.
|
|
523
523
|
|
|
524
524
|
**Requirements:**
|
|
525
525
|
- `STRIPE_SECRET_KEY` set in `functions/.env`
|
|
@@ -830,14 +830,22 @@ The `test` processor generates Stripe-shaped data and auto-fires webhooks to the
|
|
|
830
830
|
| Middleware pipeline | `src/manager/helpers/middleware.js` |
|
|
831
831
|
| Schema validation | `src/manager/helpers/settings.js` |
|
|
832
832
|
| Rate limiting | `src/manager/helpers/usage.js` |
|
|
833
|
-
| User properties | `src/manager/helpers/user.js` |
|
|
833
|
+
| User properties + schema | `src/manager/helpers/user.js` |
|
|
834
834
|
| Batch utilities | `src/manager/helpers/utilities.js` |
|
|
835
835
|
| Main API handler | `src/manager/functions/core/actions/api.js` |
|
|
836
836
|
| Config template | `templates/backend-manager-config.json` |
|
|
837
837
|
| CLI entry | `src/cli/index.js` |
|
|
838
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` |
|
|
839
844
|
| Payment processor libraries | `src/manager/libraries/payment-processors/` |
|
|
840
|
-
|
|
|
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` |
|
|
841
849
|
|
|
842
850
|
## Environment Detection
|
|
843
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,6 +1,6 @@
|
|
|
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": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"projectScripts": {
|
|
21
21
|
"start": "npx bm setup && npx bm serve",
|
|
22
22
|
"deploy": "npx bm setup && npx bm deploy",
|
|
23
|
-
"
|
|
23
|
+
"emulator": "npx bm setup && npx bm emulator",
|
|
24
24
|
"test": "npx bm setup && npx bm test",
|
|
25
25
|
"setup": "npx bm setup"
|
|
26
26
|
},
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@google-cloud/pubsub": "^5.3.0",
|
|
50
50
|
"@google-cloud/storage": "^7.19.0",
|
|
51
51
|
"@octokit/rest": "^19.0.13",
|
|
52
|
-
"@sendgrid/mail": "^
|
|
52
|
+
"@sendgrid/mail": "^8.1.6",
|
|
53
53
|
"@sentry/node": "^6.19.7",
|
|
54
54
|
"body-parser": "^1.20.4",
|
|
55
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) {
|
package/src/cli/index.js
CHANGED
|
@@ -21,7 +21,7 @@ const InstallCommand = require('./commands/install');
|
|
|
21
21
|
const ServeCommand = require('./commands/serve');
|
|
22
22
|
const DeployCommand = require('./commands/deploy');
|
|
23
23
|
const TestCommand = require('./commands/test');
|
|
24
|
-
const
|
|
24
|
+
const EmulatorCommand = require('./commands/emulator');
|
|
25
25
|
const CleanCommand = require('./commands/clean');
|
|
26
26
|
const IndexesCommand = require('./commands/indexes');
|
|
27
27
|
const WatchCommand = require('./commands/watch');
|
|
@@ -106,9 +106,9 @@ Main.prototype.process = async function (args) {
|
|
|
106
106
|
return await cmd.execute();
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
//
|
|
110
|
-
if (self.options['
|
|
111
|
-
const cmd = new
|
|
109
|
+
// Emulator (keep-alive mode)
|
|
110
|
+
if (self.options['emulator'] || self.options['emulators']) {
|
|
111
|
+
const cmd = new EmulatorCommand(self);
|
|
112
112
|
return await cmd.execute();
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const fetch = require('wonderful-fetch');
|
|
2
|
-
const moment = require('moment');
|
|
3
1
|
const { FieldValue } = require('firebase-admin/firestore');
|
|
4
2
|
|
|
5
3
|
const MAX_RETRIES = 3;
|
|
@@ -15,7 +13,9 @@ const RETRY_DELAY_MS = 1000;
|
|
|
15
13
|
* - Checks if user doc already exists (auth.uid) → skips if exists (handles test accounts, provider linking)
|
|
16
14
|
* - Batch writes user doc + increment count atomically
|
|
17
15
|
* - Retries up to 3 times with exponential backoff on failure
|
|
18
|
-
*
|
|
16
|
+
*
|
|
17
|
+
* Non-critical work (name inference, welcome emails, marketing contact) is handled
|
|
18
|
+
* by the user/signup endpoint, which the frontend calls after account creation.
|
|
19
19
|
*/
|
|
20
20
|
module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
21
21
|
const startTime = Date.now();
|
|
@@ -71,42 +71,13 @@ module.exports = async ({ Manager, assistant, user, context, libraries }) => {
|
|
|
71
71
|
await batch.commit();
|
|
72
72
|
}, MAX_RETRIES, RETRY_DELAY_MS);
|
|
73
73
|
|
|
74
|
-
assistant.log(`onCreate: Successfully created user doc for ${user.uid}`);
|
|
74
|
+
assistant.log(`onCreate: Successfully created user doc for ${user.uid} (${Date.now() - startTime}ms)`);
|
|
75
75
|
} catch (error) {
|
|
76
76
|
assistant.error(`onCreate: Failed to create user doc after ${MAX_RETRIES} retries:`, error);
|
|
77
77
|
|
|
78
78
|
// Don't reject - the user was already created in Auth
|
|
79
|
-
// The user
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Send emails in dev/production, or in test mode if TEST_EXTENDED_MODE=true
|
|
84
|
-
// Note: Must be passed to the emulator
|
|
85
|
-
const shouldSendEmails = !assistant.isTesting() || process.env.TEST_EXTENDED_MODE;
|
|
86
|
-
|
|
87
|
-
if (!shouldSendEmails) {
|
|
88
|
-
assistant.log(`onCreate: Skipping emails/SendGrid (BEM_TESTING=true, TEST_EXTENDED_MODE not set)`);
|
|
89
|
-
} else {
|
|
90
|
-
assistant.log(`onCreate: Sending emails/adding to SendGrid for ${user.uid}`);
|
|
91
|
-
|
|
92
|
-
// Add to marketing lists (SendGrid + Beehiiv) via centralized endpoint
|
|
93
|
-
fetch(`${Manager.project.apiUrl}/backend-manager/marketing/contact`, {
|
|
94
|
-
method: 'POST',
|
|
95
|
-
response: 'json',
|
|
96
|
-
body: {
|
|
97
|
-
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
98
|
-
email: user.email,
|
|
99
|
-
source: 'auth:on-create',
|
|
100
|
-
},
|
|
101
|
-
}).catch(e => assistant.error('onCreate: add-marketing-contact failed:', e));
|
|
102
|
-
|
|
103
|
-
// Send welcome emails (non-blocking, don't fail on error)
|
|
104
|
-
sendWelcomeEmail(Manager, assistant, user).catch(e => assistant.error('onCreate: sendWelcomeEmail failed:', e));
|
|
105
|
-
sendCheckupEmail(Manager, assistant, user).catch(e => assistant.error('onCreate: sendCheckupEmail failed:', e));
|
|
106
|
-
sendFeedbackEmail(Manager, assistant, user).catch(e => assistant.error('onCreate: sendFeedbackEmail failed:', e));
|
|
79
|
+
// The user/signup endpoint will handle creating the doc if it's missing
|
|
107
80
|
}
|
|
108
|
-
|
|
109
|
-
assistant.log(`onCreate: Completed for ${user.uid} (${Date.now() - startTime}ms)`);
|
|
110
81
|
};
|
|
111
82
|
|
|
112
83
|
/**
|
|
@@ -133,127 +104,3 @@ async function retryBatchWrite(assistant, fn, maxRetries, delayMs) {
|
|
|
133
104
|
|
|
134
105
|
throw lastError; // All retries failed
|
|
135
106
|
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Send welcome email (immediate)
|
|
139
|
-
*/
|
|
140
|
-
function sendWelcomeEmail(Manager, assistant, user) {
|
|
141
|
-
return fetch(`${Manager.project.apiUrl}/backend-manager/admin/email`, {
|
|
142
|
-
method: 'POST',
|
|
143
|
-
response: 'json',
|
|
144
|
-
body: {
|
|
145
|
-
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
146
|
-
to: [{ email: user.email }],
|
|
147
|
-
categories: ['account/welcome'],
|
|
148
|
-
subject: `Welcome to ${Manager.config.brand.name}!`,
|
|
149
|
-
template: 'd-b7f8da3c98ad49a2ad1e187f3a67b546',
|
|
150
|
-
group: 25928,
|
|
151
|
-
copy: false,
|
|
152
|
-
ensureUnique: true,
|
|
153
|
-
data: {
|
|
154
|
-
email: {
|
|
155
|
-
preview: `Welcome aboard! I'm Ian, the CEO and founder of ${Manager.config.brand.name}. I'm here to ensure your journey with us gets off to a great start.`,
|
|
156
|
-
},
|
|
157
|
-
body: {
|
|
158
|
-
title: `Welcome to ${Manager.config.brand.name}!`,
|
|
159
|
-
message: `
|
|
160
|
-
Welcome aboard!
|
|
161
|
-
<br><br>
|
|
162
|
-
I'm Ian, the founder and CEO of <strong>${Manager.config.brand.name}</strong>, and I'm thrilled to have you with us.
|
|
163
|
-
Your journey begins today, and we are committed to supporting you every step of the way.
|
|
164
|
-
<br><br>
|
|
165
|
-
We are dedicated to ensuring your experience is exceptional.
|
|
166
|
-
Feel free to reply directly to this email with any questions you may have.
|
|
167
|
-
<br><br>
|
|
168
|
-
Thank you for choosing <strong>${Manager.config.brand.name}</strong>. Here's to new beginnings!
|
|
169
|
-
`,
|
|
170
|
-
},
|
|
171
|
-
signoff: {
|
|
172
|
-
type: 'personal',
|
|
173
|
-
name: 'Ian Wiedenman, CEO',
|
|
174
|
-
url: `https://ianwiedenman.com?utm_source=welcome-email&utm_medium=email&utm_campaign=${Manager.config.app.id}`,
|
|
175
|
-
urlText: '@ianwieds',
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
})
|
|
180
|
-
.then((json) => {
|
|
181
|
-
assistant.log('sendWelcomeEmail(): Success', json);
|
|
182
|
-
return json;
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Send checkup email (7 days after signup)
|
|
188
|
-
*/
|
|
189
|
-
function sendCheckupEmail(Manager, assistant, user) {
|
|
190
|
-
return fetch(`${Manager.project.apiUrl}/backend-manager/admin/email`, {
|
|
191
|
-
method: 'POST',
|
|
192
|
-
response: 'json',
|
|
193
|
-
body: {
|
|
194
|
-
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
195
|
-
to: [{ email: user.email }],
|
|
196
|
-
categories: ['account/checkup'],
|
|
197
|
-
subject: `How's your experience with ${Manager.config.brand.name}?`,
|
|
198
|
-
template: 'd-b7f8da3c98ad49a2ad1e187f3a67b546',
|
|
199
|
-
group: 25928,
|
|
200
|
-
copy: false,
|
|
201
|
-
ensureUnique: true,
|
|
202
|
-
sendAt: moment().add(7, 'days').unix(),
|
|
203
|
-
data: {
|
|
204
|
-
email: {
|
|
205
|
-
preview: `Checking in from ${Manager.config.brand.name} to see how things are going. Let us know if you have any questions or feedback!`,
|
|
206
|
-
},
|
|
207
|
-
body: {
|
|
208
|
-
title: `How's everything going?`,
|
|
209
|
-
message: `
|
|
210
|
-
Hi there,
|
|
211
|
-
<br><br>
|
|
212
|
-
It's Ian again from <strong>${Manager.config.brand.name}</strong>. Just checking in to see how things are going for you.
|
|
213
|
-
<br><br>
|
|
214
|
-
Have you had a chance to explore all our features? Any questions or feedback for us?
|
|
215
|
-
<br><br>
|
|
216
|
-
We're always here to help, so don't hesitate to reach out. Just reply to this email and we'll get back to you as soon as possible.
|
|
217
|
-
<br><br>
|
|
218
|
-
Thank you for choosing <strong>${Manager.config.brand.name}</strong>. Here's to new beginnings!
|
|
219
|
-
`,
|
|
220
|
-
},
|
|
221
|
-
signoff: {
|
|
222
|
-
type: 'personal',
|
|
223
|
-
name: 'Ian Wiedenman, CEO',
|
|
224
|
-
url: `https://ianwiedenman.com?utm_source=checkup-email&utm_medium=email&utm_campaign=${Manager.config.app.id}`,
|
|
225
|
-
urlText: '@ianwieds',
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
})
|
|
230
|
-
.then((json) => {
|
|
231
|
-
assistant.log('sendCheckupEmail(): Success', json);
|
|
232
|
-
return json;
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Send feedback email (14 days after signup)
|
|
238
|
-
*/
|
|
239
|
-
function sendFeedbackEmail(Manager, assistant, user) {
|
|
240
|
-
return fetch(`${Manager.project.apiUrl}/backend-manager/admin/email`, {
|
|
241
|
-
method: 'POST',
|
|
242
|
-
response: 'json',
|
|
243
|
-
body: {
|
|
244
|
-
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
245
|
-
to: [{ email: user.email }],
|
|
246
|
-
categories: ['engagement/feedback'],
|
|
247
|
-
subject: `Want to share your feedback about ${Manager.config.brand.name}?`,
|
|
248
|
-
template: 'd-c1522214c67b47058669acc5a81ed663',
|
|
249
|
-
group: 25928,
|
|
250
|
-
copy: false,
|
|
251
|
-
ensureUnique: true,
|
|
252
|
-
sendAt: moment().add(14, 'days').unix(),
|
|
253
|
-
},
|
|
254
|
-
})
|
|
255
|
-
.then((json) => {
|
|
256
|
-
assistant.log('sendFeedbackEmail(): Success', json);
|
|
257
|
-
return json;
|
|
258
|
-
});
|
|
259
|
-
}
|