backend-manager 5.0.193 → 5.0.195

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 CHANGED
@@ -14,6 +14,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
+ # [5.0.195] - 2026-04-10
18
+ ### Fixed
19
+ - 24-hour cancellation guard in `payments/cancel` was comparing `Date.now()` (milliseconds) against `startDateUNIX` (seconds), producing an "age" of ~56 years for every subscription — guard never fired and users could cancel brand-new subscriptions. Now multiplies `startDateUNIX` by 1000 before subtraction.
20
+ ### Changed
21
+ - Standardized CLI examples in `CLAUDE.md` and `README.md` to use `npx mgr` instead of the deprecated `npx bm` alias
22
+
23
+ # [5.0.194] - 2026-04-08
24
+ ### Fixed
25
+ - Fix email template data merge: caller's `settings.data` is now deep-merged at root of template data tree, removing the broken `data.` prefix indirection that caused empty order confirmation emails since 5.0.185
26
+ ### Added
27
+ - `preview` as a top-level setting on `email.send()` (alongside `subject`)
28
+ - `logs:read` CLI: `--search`, `--order`, `--filter` flags and increased default limit to 300
29
+ ### Changed
30
+ - Email templates now access caller data at root (`{{order.id}}`, `{{body.message}}`) instead of under `data.*`
31
+
17
32
  # [5.0.192] - 2026-04-02
18
33
  ### Added
19
34
  - Setup test to create `hooks/auth/` and `hooks/cron/daily/` directories in consumer projects during `npx bm setup`
package/CLAUDE.md CHANGED
@@ -600,30 +600,30 @@ The `POST /admin/post` route creates blog posts via GitHub's API. It handles ima
600
600
  ### Running Tests
601
601
  ```bash
602
602
  # Option 1: Two terminals
603
- npx bm emulator # Terminal 1 - keeps emulator running
604
- npx bm test # Terminal 2 - runs tests
603
+ npx mgr emulator # Terminal 1 - keeps emulator running
604
+ npx mgr test # Terminal 2 - runs tests
605
605
 
606
606
  # Option 2: Single command (auto-starts emulator)
607
- npx bm test
607
+ npx mgr test
608
608
  ```
609
609
 
610
610
  ### Log Files
611
611
  BEM CLI commands automatically save all output to log files in `functions/` while still streaming to the console:
612
- - **`functions/serve.log`** — Output from `npx bm serve` (Firebase serve)
612
+ - **`functions/serve.log`** — Output from `npx mgr serve` (Firebase serve)
613
613
  - **`functions/emulator.log`** — Full emulator output (Firebase emulator + Cloud Functions logs)
614
614
  - **`functions/test.log`** — Test runner output (when running against an existing emulator)
615
- - **`functions/logs.log`** — Cloud Function logs from `npx bm logs:read` or `npx bm logs:tail` (raw JSON for `read`, streaming text for `tail`)
615
+ - **`functions/logs.log`** — Cloud Function logs from `npx mgr logs:read` or `npx mgr logs:tail` (raw JSON for `read`, streaming text for `tail`)
616
616
 
617
- When `npx bm test` starts its own emulator, logs go to `emulator.log` (since it delegates to the emulator command). When running against an already-running emulator, logs go to `test.log`.
617
+ When `npx mgr test` starts its own emulator, logs go to `emulator.log` (since it delegates to the emulator command). When running against an already-running emulator, logs go to `test.log`.
618
618
 
619
619
  These files are overwritten on each run and are gitignored (`*.log`). Use them to search for errors, debug webhook pipelines, or review full function output after a test run.
620
620
 
621
621
  ### Filtering Tests
622
622
  ```bash
623
- npx bm test rules/ # Run rules tests (both BEM and project)
624
- npx bm test bem:rules/ # Only BEM's rules tests
625
- npx bm test project:rules/ # Only project's rules tests
626
- npx bm test user/ admin/ # Multiple paths
623
+ npx mgr test rules/ # Run rules tests (both BEM and project)
624
+ npx mgr test bem:rules/ # Only BEM's rules tests
625
+ npx mgr test project:rules/ # Only project's rules tests
626
+ npx mgr test user/ admin/ # Multiple paths
627
627
  ```
628
628
 
629
629
  ### Test Locations
@@ -716,7 +716,7 @@ assert.fail(message) // Explicit fail
716
716
 
717
717
  ## Stripe Webhook Forwarding
718
718
 
719
- 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.
719
+ BEM auto-starts Stripe CLI webhook forwarding when running `npx mgr serve` or `npx mgr emulator`. This forwards Stripe test webhooks to the local server so the full payment pipeline works end-to-end during development.
720
720
 
721
721
  **Requirements:**
722
722
  - `STRIPE_SECRET_KEY` set in `functions/.env`
@@ -725,7 +725,7 @@ BEM auto-starts Stripe CLI webhook forwarding when running `npx bm serve` or `np
725
725
 
726
726
  **Standalone usage:**
727
727
  ```bash
728
- npx bm stripe
728
+ npx mgr stripe
729
729
  ```
730
730
 
731
731
  If any prerequisite is missing, webhook forwarding is silently skipped with an info message.
@@ -736,26 +736,28 @@ The forwarding URL is: `http://localhost:{hostingPort}/backend-manager/payments/
736
736
 
737
737
  Quick commands for reading/writing Firestore and managing Auth users directly from the terminal. Works in any BEM consumer project (requires `functions/service-account.json` for production, or `--emulator` for local).
738
738
 
739
+ **IMPORTANT: All CLI commands (`npx mgr ...`) MUST be run from the consumer project's `functions/` subdirectory** (e.g., `cd /path/to/my-project/functions && npx mgr ...`). The `mgr` binary lives in `functions/node_modules/.bin/` — running from the project root or any other directory will fail.
740
+
739
741
  ### Firestore Commands
740
742
 
741
743
  ```bash
742
- npx bm firestore:get <path> # Read a document
743
- npx bm firestore:set <path> '<json>' # Write/merge a document
744
- npx bm firestore:set <path> '<json>' --no-merge # Overwrite a document entirely
745
- npx bm firestore:query <collection> # Query a collection (default limit 25)
744
+ npx mgr firestore:get <path> # Read a document
745
+ npx mgr firestore:set <path> '<json>' # Write/merge a document
746
+ npx mgr firestore:set <path> '<json>' --no-merge # Overwrite a document entirely
747
+ npx mgr firestore:query <collection> # Query a collection (default limit 25)
746
748
  --where "field==value" # Filter (repeatable for AND)
747
749
  --orderBy "field:desc" # Sort
748
750
  --limit N # Limit results
749
- npx bm firestore:delete <path> # Delete a document (prompts for confirmation)
751
+ npx mgr firestore:delete <path> # Delete a document (prompts for confirmation)
750
752
  ```
751
753
 
752
754
  ### Auth Commands
753
755
 
754
756
  ```bash
755
- npx bm auth:get <uid-or-email> # Get user by UID or email (auto-detected via @)
756
- npx bm auth:list [--limit N] [--page-token T] # List users (default 100)
757
- npx bm auth:delete <uid-or-email> # Delete user (prompts for confirmation)
758
- npx bm auth:set-claims <uid-or-email> '<json>' # Set custom claims
757
+ npx mgr auth:get <uid-or-email> # Get user by UID or email (auto-detected via @)
758
+ npx mgr auth:list [--limit N] [--page-token T] # List users (default 100)
759
+ npx mgr auth:delete <uid-or-email> # Delete user (prompts for confirmation)
760
+ npx mgr auth:set-claims <uid-or-email> '<json>' # Set custom claims
759
761
  ```
760
762
 
761
763
  ### Logs Commands
@@ -763,24 +765,63 @@ npx bm auth:set-claims <uid-or-email> '<json>' # Set custom claims
763
765
  Fetch or stream Cloud Function logs from Google Cloud Logging. Requires `gcloud` CLI installed and authenticated. Auto-resolves the project ID from `service-account.json`, `.firebaserc`, or `GCLOUD_PROJECT`.
764
766
 
765
767
  ```bash
766
- npx bm logs:read # Read last 1h of logs (default: 50 entries)
767
- npx bm logs:read --fn bm_api # Filter by function name
768
- npx bm logs:read --fn bm_api --severity ERROR # Filter by severity (DEBUG, INFO, WARNING, ERROR, CRITICAL)
769
- npx bm logs:read --since 2d --limit 100 # Custom time range and limit
770
- npx bm logs:tail # Stream live logs
771
- npx bm logs:tail --fn bm_paymentsWebhookOnWrite # Stream filtered live logs
768
+ npx mgr logs:read # Read last 1h of logs (default: 300 entries, newest first)
769
+ npx mgr logs:read --fn bm_api # Filter by function name
770
+ npx mgr logs:read --fn bm_api --severity ERROR # Filter by severity (DEBUG, INFO, WARNING, ERROR, CRITICAL)
771
+ npx mgr logs:read --since 2d --limit 100 # Custom time range and limit
772
+ npx mgr logs:read --search "72.134.242.25" # Search textPayload for a string (IP, email, error, etc.)
773
+ npx mgr logs:read --fn bm_authBeforeCreate --search "ian@example.com" --since 7d # Combined filters
774
+ npx mgr logs:read --order asc # Oldest first (default: desc/newest first)
775
+ npx mgr logs:read --filter 'jsonPayload.level="error"' # Raw gcloud filter passthrough
776
+ npx mgr logs:tail # Stream live logs
777
+ npx mgr logs:tail --fn bm_paymentsWebhookOnWrite # Stream filtered live logs
772
778
  ```
773
779
 
774
780
  Both commands save output to `functions/logs.log` (overwritten on each run). `logs:read` saves raw JSON; `logs:tail` streams text.
775
781
 
782
+ **Cloud Logs vs Local Logs:** These commands query **production** Google Cloud Logging. For **local/dev** logs, read `functions/serve.log` (from `npx mgr serve`) or `functions/emulator.log` (from `npx mgr test`) directly — they are plain text files, not gcloud.
783
+
776
784
  | Flag | Description | Default | Commands |
777
785
  |------|-------------|---------|----------|
778
- | `--fn <name>` | Filter by Cloud Function name | all | both |
779
- | `--severity <level>` | Minimum severity level | all | both |
786
+ | `--fn <name>` | Filter by Cloud Function name (see table below) | all | both |
787
+ | `--severity <level>` | Minimum severity: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` | all | both |
788
+ | `--search <text>` | Search textPayload for a substring (IP, email, uid, error message) | none | both |
789
+ | `--filter <expr>` | Raw gcloud logging filter expression (appended to built-in filters) | none | both |
780
790
  | `--since <duration>` | Time range (`30m`, `1h`, `2d`, `1w`) | `1h` | read only |
781
- | `--limit <n>` | Max entries | `50` | read only |
791
+ | `--limit <n>` | Max entries | `300` | read only |
792
+ | `--order <dir>` | Sort order: `asc` (oldest first) or `desc` (newest first) | `desc` | read only |
793
+ | `--interval <sec>` | Polling interval in seconds | `5` | tail only |
782
794
  | `--raw` | Output raw JSON | false | both |
783
795
 
796
+ #### `--fn` Function Name Reference
797
+
798
+ The `--fn` flag uses the **deployed Cloud Function name**, not the route path.
799
+
800
+ **BEM built-in functions (always deployed):**
801
+
802
+ | Function name | Type | Description |
803
+ |---------------|------|-------------|
804
+ | `bm_api` | HTTPS | Main API router — all consumer routes (GET/POST/PUT/DELETE) go through this |
805
+ | `bm_authBeforeCreate` | Auth blocking | Before user creation: disposable email blocking, IP rate limiting, consumer hooks |
806
+ | `bm_authBeforeSignIn` | Auth blocking | Before sign-in: consumer hooks |
807
+ | `bm_authOnCreate` | Auth event | After user creation: user doc setup |
808
+ | `bm_authOnDelete` | Auth event | After user deletion |
809
+ | `bm_paymentsWebhookOnWrite` | Firestore trigger | Processes payment webhooks |
810
+ | `bm_paymentsDisputeOnWrite` | Firestore trigger | Processes payment disputes |
811
+ | `bm_notificationsOnWrite` | Firestore trigger | Sends push notifications |
812
+ | `bm_cronDaily` | Scheduled | Daily cron (midnight UTC) |
813
+ | `bm_cronFrequent` | Scheduled | Frequent cron (every 10 min) |
814
+
815
+ **Consumer-defined functions** use the export name from `functions/index.js` (e.g., `exports.items = ...` → `--fn items`).
816
+
817
+ **Quick lookup — which function to query:**
818
+ - API route errors → `--fn bm_api`
819
+ - Signup/auth blocked → `--fn bm_authBeforeCreate`
820
+ - Sign-in issues → `--fn bm_authBeforeSignIn`
821
+ - User doc not created → `--fn bm_authOnCreate`
822
+ - Payment not processing → `--fn bm_paymentsWebhookOnWrite`
823
+ - Cron job issues → `--fn bm_cronDaily` or `--fn bm_cronFrequent`
824
+
784
825
  ### Shared Flags
785
826
 
786
827
  | Flag | Description |
@@ -793,22 +834,22 @@ Both commands save output to `functions/logs.log` (overwritten on each run). `lo
793
834
 
794
835
  ```bash
795
836
  # Read a user document from production
796
- npx bm firestore:get users/abc123
837
+ npx mgr firestore:get users/abc123
797
838
 
798
839
  # Write to emulator
799
- npx bm firestore:set users/test123 '{"name":"Test User"}' --emulator
840
+ npx mgr firestore:set users/test123 '{"name":"Test User"}' --emulator
800
841
 
801
842
  # Query with filters
802
- npx bm firestore:query users --where "subscription.status==active" --limit 10
843
+ npx mgr firestore:query users --where "subscription.status==active" --limit 10
803
844
 
804
845
  # Look up auth user by email
805
- npx bm auth:get user@example.com
846
+ npx mgr auth:get user@example.com
806
847
 
807
848
  # Set admin claims
808
- npx bm auth:set-claims user@example.com '{"admin":true}'
849
+ npx mgr auth:set-claims user@example.com '{"admin":true}'
809
850
 
810
851
  # Delete from emulator (no confirmation needed)
811
- npx bm firestore:delete users/test123 --emulator
852
+ npx mgr firestore:delete users/test123 --emulator
812
853
  ```
813
854
 
814
855
  ## Usage & Rate Limiting
@@ -1325,7 +1366,7 @@ Campaigns reference segments by SSOT key: `segments: ['subscription_free']`. Aut
1325
1366
 
1326
1367
  ### Seed Campaigns
1327
1368
 
1328
- Created by `npx bm setup` (idempotent, enforced fields checked every run):
1369
+ Created by `npx mgr setup` (idempotent, enforced fields checked every run):
1329
1370
 
1330
1371
  | ID | Type | Description |
1331
1372
  |----|------|-------------|
@@ -1378,7 +1419,7 @@ marketing: {
1378
1419
 
1379
1420
  8. **Increment usage before update** - Call `usage.increment()` then `usage.update()`
1380
1421
 
1381
- 9. **Add Firestore composite indexes for new compound queries** - Any new Firestore query using multiple `.where()` clauses or `.where()` + `.orderBy()` requires a composite index. Add it to `src/cli/commands/setup-tests/helpers/required-indexes.js` (the SSOT). Consumer projects pick these up via `npx bm setup`, which syncs them into `firestore.indexes.json`. Without the index, the query will crash with `FAILED_PRECONDITION` in production.
1422
+ 9. **Add Firestore composite indexes for new compound queries** - Any new Firestore query using multiple `.where()` clauses or `.where()` + `.orderBy()` requires a composite index. Add it to `src/cli/commands/setup-tests/helpers/required-indexes.js` (the SSOT). Consumer projects pick these up via `npx mgr setup`, which syncs them into `firestore.indexes.json`. Without the index, the query will crash with `FAILED_PRECONDITION` in production.
1382
1423
 
1383
1424
  ## Key Files Reference
1384
1425
 
@@ -1422,7 +1463,7 @@ marketing: {
1422
1463
  ```javascript
1423
1464
  assistant.isDevelopment() // true when ENVIRONMENT !== 'production' or in emulator
1424
1465
  assistant.isProduction() // true when ENVIRONMENT === 'production'
1425
- assistant.isTesting() // true when running tests (via npx bm test)
1466
+ assistant.isTesting() // true when running tests (via npx mgr test)
1426
1467
  ```
1427
1468
 
1428
1469
  ## Model Context Protocol (MCP)
@@ -1432,7 +1473,7 @@ BEM includes a built-in MCP server that exposes BEM routes as tools for Claude C
1432
1473
  ### Architecture
1433
1474
 
1434
1475
  Two transport modes:
1435
- - **Stdio** (local): `npx bm mcp` — for Claude Code / Claude Desktop
1476
+ - **Stdio** (local): `npx mgr mcp` — for Claude Code / Claude Desktop
1436
1477
  - **Streamable HTTP** (remote): `POST /backend-manager/mcp` — for Claude Chat (stateless, Firebase Functions compatible)
1437
1478
 
1438
1479
  ### Available Tools (19)
@@ -1466,7 +1507,7 @@ Two transport modes:
1466
1507
 
1467
1508
  ### Hosting Rewrites
1468
1509
 
1469
- The `npx bm setup` command automatically adds required Firebase Hosting rewrites for MCP OAuth:
1510
+ The `npx mgr setup` command automatically adds required Firebase Hosting rewrites for MCP OAuth:
1470
1511
  ```json
1471
1512
  {
1472
1513
  "source": "{/backend-manager,/backend-manager/**,/.well-known/oauth-protected-resource,/.well-known/oauth-authorization-server,/authorize,/token}",
@@ -1477,7 +1518,7 @@ The `npx bm setup` command automatically adds required Firebase Hosting rewrites
1477
1518
  ### CLI Usage
1478
1519
 
1479
1520
  ```bash
1480
- npx bm mcp # Start stdio MCP server (for Claude Code)
1521
+ npx mgr mcp # Start stdio MCP server (for Claude Code)
1481
1522
  ```
1482
1523
 
1483
1524
  ### Claude Code Configuration
package/README.md CHANGED
@@ -89,7 +89,7 @@ module.exports = function (assistant) {
89
89
  Run the setup command:
90
90
 
91
91
  ```bash
92
- npx bm setup
92
+ npx mgr setup
93
93
  ```
94
94
 
95
95
  ## Initialization Options
@@ -802,28 +802,28 @@ BEM includes an integration test framework that runs against the Firebase emulat
802
802
 
803
803
  ```bash
804
804
  # Option 1: Two terminals (recommended for development)
805
- npx bm emulator # Terminal 1 - keeps emulator running
806
- npx bm test # Terminal 2 - runs tests
805
+ npx mgr emulator # Terminal 1 - keeps emulator running
806
+ npx mgr test # Terminal 2 - runs tests
807
807
 
808
808
  # Option 2: Single command (auto-starts emulator, shuts down after)
809
- npx bm test
809
+ npx mgr test
810
810
  ```
811
811
 
812
812
  ### Filtering Tests
813
813
 
814
814
  ```bash
815
- npx bm test rules/ # Run rules tests (both BEM and project)
816
- npx bm test bem:rules/ # Only BEM's rules tests
817
- npx bm test project:rules/ # Only project's rules tests
818
- npx bm test user/ admin/ # Multiple paths
815
+ npx mgr test rules/ # Run rules tests (both BEM and project)
816
+ npx mgr test bem:rules/ # Only BEM's rules tests
817
+ npx mgr test project:rules/ # Only project's rules tests
818
+ npx mgr test user/ admin/ # Multiple paths
819
819
  ```
820
820
 
821
821
  ### Log Files
822
822
 
823
823
  BEM CLI commands automatically save output to log files in the project directory:
824
- - **`emulator.log`** — Full emulator + Cloud Functions output (`npx bm emulator`)
825
- - **`test.log`** — Test runner output (`npx bm test`, when running against an existing emulator)
826
- - **`logs.log`** — Cloud Function logs (`npx bm logs:read` or `npx bm logs:tail`)
824
+ - **`emulator.log`** — Full emulator + Cloud Functions output (`npx mgr emulator`)
825
+ - **`test.log`** — Test runner output (`npx mgr test`, when running against an existing emulator)
826
+ - **`logs.log`** — Cloud Function logs (`npx mgr logs:read` or `npx mgr logs:tail`)
827
827
 
828
828
  Logs are overwritten on each run. Use them to debug failing tests or review function output.
829
829
 
package/TODO-2.md CHANGED
@@ -23,6 +23,26 @@ waht about when they request a cancel
23
23
  Read cancellation-requested.js
24
24
  The category is order/cancellation-requested (line 13).
25
25
 
26
+ ----
27
+ add a dedicated BEM JSON field for usage to reset
28
+ * this way we can have clear LIMITS with their definitions like
29
+ * [
30
+ {
31
+ name: 'credits'
32
+ reset: true,
33
+ },
34
+ {
35
+ name: 'agents',
36
+ reset: false,
37
+ }
38
+ ]
39
+ * mirrors: [
40
+ {
41
+ collection: 'agents',
42
+ fields: ['usage.credits.daily', 'runs.replies.daily],
43
+ }
44
+ ]
45
+
26
46
  ---
27
47
  MIRROR settigns in BEM JSON so that usage reset can properly get MIRRED DOCS liek slapform forms or chatsy agents DOCS
28
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.193",
3
+ "version": "5.0.195",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -56,11 +56,12 @@ class LogsCommand extends BaseCommand {
56
56
 
57
57
  /**
58
58
  * Fetch historical logs.
59
- * Usage: npx bm logs:read [--fn bm_api] [--severity ERROR] [--since 1h] [--limit 300]
59
+ * Usage: npx bm logs:read [--fn bm_api] [--severity ERROR] [--since 1h] [--limit 300] [--search "text"] [--order desc] [--filter 'raw gcloud filter']
60
60
  */
61
61
  async read(projectId, argv) {
62
62
  const filter = this.buildFilter(argv);
63
63
  const limit = parseInt(argv.limit, 10) || 300;
64
+ const order = argv.order || 'desc';
64
65
 
65
66
  const cmd = [
66
67
  'gcloud', 'logging', 'read',
@@ -68,7 +69,7 @@ class LogsCommand extends BaseCommand {
68
69
  `--project=${projectId}`,
69
70
  `--limit=${limit}`,
70
71
  '--format=json',
71
- '--order=asc',
72
+ `--order=${order}`,
72
73
  ].filter(Boolean).join(' ');
73
74
 
74
75
  // Set up log file in the project directory
@@ -83,7 +84,7 @@ class LogsCommand extends BaseCommand {
83
84
  const output = execSync(cmd, {
84
85
  encoding: 'utf8',
85
86
  maxBuffer: 10 * 1024 * 1024, // 10MB
86
- timeout: 30000,
87
+ timeout: 60000,
87
88
  });
88
89
 
89
90
  const entries = JSON.parse(output || '[]');
@@ -233,6 +234,16 @@ class LogsCommand extends BaseCommand {
233
234
  parts.push(`severity>=${argv.severity.toUpperCase()}`);
234
235
  }
235
236
 
237
+ // Text search filter (searches textPayload)
238
+ if (argv.search) {
239
+ parts.push(`textPayload:"${argv.search}"`);
240
+ }
241
+
242
+ // Raw filter passthrough (appended as-is)
243
+ if (argv.filter) {
244
+ parts.push(argv.filter);
245
+ }
246
+
236
247
  // Timestamp filter (read only, not tail)
237
248
  if (!options.excludeTimestamp) {
238
249
  const since = argv.since || '1h';
@@ -3152,6 +3152,7 @@
3152
3152
  "moneypipe.net",
3153
3153
  "mongrec.top",
3154
3154
  "monmail.fr.nf",
3155
+ "monomoho.site",
3155
3156
  "monumentmail.com",
3156
3157
  "moolee.net",
3157
3158
  "moonapps.org",
@@ -142,6 +142,8 @@ Transactional.prototype.build = async function (settings) {
142
142
  throw errorWithCode('Parameter subject is required', 400);
143
143
  }
144
144
 
145
+ const preview = settings.preview || settings?.data?.email?.preview || null;
146
+
145
147
  const templateId = TEMPLATES[settings.template] || settings.template || TEMPLATES['default'];
146
148
 
147
149
  // Resolve sender category
@@ -176,7 +178,7 @@ Transactional.prototype.build = async function (settings) {
176
178
  const unsubSig = crypto.createHmac('sha256', process.env.UNSUBSCRIBE_HMAC_KEY).update(to[0].email.toLowerCase()).digest('hex');
177
179
  const unsubscribeUrl = `${Manager.project.websiteUrl}/portal/email-preferences?email=${encode(to[0].email)}&asmId=${encode(groupId)}&templateId=${encode(templateId)}&sig=${unsubSig}`;
178
180
 
179
- // Build signoff
181
+ // Build signoff defaults
180
182
  const signoff = settings?.data?.signoff || {};
181
183
  signoff.type = signoff.type || 'team';
182
184
 
@@ -188,12 +190,12 @@ Transactional.prototype.build = async function (settings) {
188
190
  signoff.urlText = signoff.urlText || '@ianwieds';
189
191
  }
190
192
 
191
- // Build dynamic template data defaults
193
+ // Build dynamic template data — system-generated defaults
192
194
  const dynamicTemplateData = {
193
195
  email: {
194
196
  id: Manager.require('uuid').v4(),
195
197
  subject,
196
- preview: null,
198
+ preview,
197
199
  body: null,
198
200
  unsubscribeUrl,
199
201
  categories,
@@ -209,18 +211,20 @@ Transactional.prototype.build = async function (settings) {
209
211
  signoff,
210
212
  brand: brandData,
211
213
  user: userProperties,
212
- data: {},
213
214
  };
214
215
 
215
- // Deep-merge caller's data on top so they can override any field
216
- // (e.g. email.preview, email.subject, personalization.name, data.body.*, etc.)
216
+ // Deep-merge caller's data on top of defaults.
217
+ // This is the single template data tree — everything the template can access.
218
+ // Callers can override any field (email.preview, signoff.type, etc.)
219
+ // and add custom data (order.*, body.*, abandonedCart.*, etc.) at the root.
220
+ // Templates access all fields at the root: {{order.id}}, {{email.preview}}, {{brand.name}}.
217
221
  if (settings.data) {
218
222
  _.merge(dynamicTemplateData, settings.data);
219
223
  }
220
224
 
221
- // Process markdown in body fields (after merge so all data paths are resolved)
222
- if (dynamicTemplateData.data?.body?.message) {
223
- dynamicTemplateData.data.body.message = md.render(dynamicTemplateData.data.body.message);
225
+ // Process markdown in body fields (after merge so caller data is resolved)
226
+ if (dynamicTemplateData.body?.message) {
227
+ dynamicTemplateData.body.message = md.render(dynamicTemplateData.body.message);
224
228
  }
225
229
  if (dynamicTemplateData.email?.body) {
226
230
  dynamicTemplateData.email.body = md.render(dynamicTemplateData.email.body);
@@ -235,8 +239,8 @@ Transactional.prototype.build = async function (settings) {
235
239
  utm: settings.utm,
236
240
  };
237
241
 
238
- if (dynamicTemplateData.data?.body?.message) {
239
- dynamicTemplateData.data.body.message = tagLinks(dynamicTemplateData.data.body.message, utmOptions);
242
+ if (dynamicTemplateData.body?.message) {
243
+ dynamicTemplateData.body.message = tagLinks(dynamicTemplateData.body.message, utmOptions);
240
244
  }
241
245
  if (dynamicTemplateData.email?.body) {
242
246
  dynamicTemplateData.email.body = tagLinks(dynamicTemplateData.email.body, utmOptions);
@@ -35,7 +35,7 @@ module.exports = async ({ assistant, user, settings }) => {
35
35
  // Guard: subscription younger than 24 hours
36
36
  const startDateUNIX = subscription.payment?.startDate?.timestampUNIX;
37
37
  if (startDateUNIX) {
38
- const ageMs = Date.now() - startDateUNIX;
38
+ const ageMs = Date.now() - (startDateUNIX * 1000);
39
39
  const twentyFourHoursMs = 24 * 60 * 60 * 1000;
40
40
  if (ageMs < twentyFourHoursMs) {
41
41
  assistant.log(`Cancel rejected: uid=${uid}, subscription is only ${Math.round(ageMs / 1000 / 60)} minutes old`);