backend-manager 5.1.4 → 5.2.0
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/.claude/settings.local.json +12 -0
- package/CHANGELOG.md +23 -0
- package/CLAUDE.md +2 -1
- package/README.md +15 -0
- package/docs/common-mistakes.md +1 -0
- package/docs/consent.md +333 -0
- package/docs/testing.md +36 -0
- package/package.json +1 -1
- package/src/cli/commands/emulator.js +44 -8
- package/src/cli/commands/serve.js +73 -7
- package/src/cli/commands/test.js +47 -1
- package/src/cli/commands/watch.js +15 -3
- package/src/manager/helpers/user.js +29 -0
- package/src/manager/index.js +29 -0
- package/src/manager/libraries/email/data/disposable-domains.json +8 -0
- package/src/manager/libraries/email/generators/newsletter.js +2 -2
- package/src/manager/libraries/email/providers/beehiiv.js +1 -0
- package/src/manager/libraries/payment/processors/stripe.js +12 -0
- package/src/manager/libraries/payment/processors/test.js +8 -1
- package/src/manager/routes/admin/infer-contact/post.js +3 -2
- package/src/manager/routes/marketing/email-preferences/post.js +165 -37
- package/src/manager/routes/marketing/webhook/forward/post.js +168 -0
- package/src/manager/routes/marketing/webhook/post.js +180 -0
- package/src/manager/routes/marketing/webhook/processors/beehiiv.js +196 -0
- package/src/manager/routes/marketing/webhook/processors/sendgrid.js +171 -0
- package/src/manager/routes/payments/cancel/post.js +2 -2
- package/src/manager/routes/payments/cancel/processors/test.js +5 -2
- package/src/manager/routes/payments/intent/processors/test.js +7 -3
- package/src/manager/routes/payments/refund/processors/test.js +4 -1
- package/src/manager/routes/user/signup/post.js +65 -1
- package/src/manager/schemas/marketing/email-preferences/post.js +8 -4
- package/src/manager/schemas/marketing/webhook/forward/post.js +8 -0
- package/src/manager/schemas/marketing/webhook/post.js +7 -0
- package/src/manager/schemas/payments/cancel/post.js +5 -0
- package/src/manager/schemas/user/signup/post.js +5 -0
- package/src/test/runner.js +61 -18
- package/src/test/test-accounts.js +94 -12
- package/src/test/utils/http-client.js +4 -3
- package/test/events/payments/journey-payments-cancel-endpoint.js +3 -12
- package/test/events/payments/journey-payments-cancel.js +4 -5
- package/test/events/payments/journey-payments-failure.js +0 -1
- package/test/events/payments/journey-payments-one-time-failure.js +6 -3
- package/test/events/payments/journey-payments-one-time.js +6 -3
- package/test/events/payments/journey-payments-plan-change.js +5 -5
- package/test/events/payments/journey-payments-refund-webhook.js +2 -3
- package/test/events/payments/journey-payments-suspend.js +4 -5
- package/test/events/payments/journey-payments-trial-cancel.js +3 -12
- package/test/events/payments/journey-payments-trial.js +2 -3
- package/test/events/payments/journey-payments-uid-resolution.js +2 -3
- package/test/functions/admin/database-read.js +0 -14
- package/test/functions/admin/database-write.js +0 -14
- package/test/functions/admin/firestore-query.js +0 -14
- package/test/functions/admin/firestore-read.js +0 -15
- package/test/functions/admin/firestore-write.js +0 -11
- package/test/functions/general/add-marketing-contact.js +16 -14
- package/test/helpers/email.js +1 -1
- package/test/helpers/infer-contact.js +3 -3
- package/test/helpers/user.js +241 -2
- package/test/helpers/webhook-forward.js +392 -0
- package/test/marketing/newsletter-generate.js +17 -7
- package/test/routes/admin/database.js +0 -13
- package/test/routes/admin/firestore-query.js +0 -13
- package/test/routes/admin/firestore.js +0 -14
- package/test/routes/admin/infer-contact.js +6 -3
- package/test/routes/admin/post.js +4 -2
- package/test/routes/marketing/contact.js +60 -26
- package/test/routes/marketing/email-preferences.js +145 -69
- package/test/routes/marketing/webhook-forward.js +54 -0
- package/test/routes/marketing/webhook.js +582 -0
- package/test/routes/payments/cancel.js +2 -7
- package/test/routes/payments/dispute-alert.js +0 -39
- package/test/routes/payments/refund.js +3 -1
- package/test/routes/payments/webhook.js +5 -26
- package/test/routes/test/usage.js +2 -2
- package/test/routes/user/signup.js +114 -0
|
@@ -25,11 +25,63 @@ class ServeCommand extends BaseCommand {
|
|
|
25
25
|
// Start Stripe webhook forwarding in background
|
|
26
26
|
this.startStripeWebhookForwarding();
|
|
27
27
|
|
|
28
|
-
// Set up log file in the project directory
|
|
28
|
+
// Set up log file in the project directory.
|
|
29
|
+
// Mirrors the emulator.js pattern: the file is truncated on boot and on every
|
|
30
|
+
// hot reload. Two reset signals are honored:
|
|
31
|
+
// 1. Sentinel file (serve.log.reset) — used by the BEM watcher when source
|
|
32
|
+
// changes in backend-manager itself trigger a reload.
|
|
33
|
+
// 2. Reload marker on stdout (`Using node@22 from host.`) — catches reloads
|
|
34
|
+
// triggered by firebase serve's own internal watcher (any change inside
|
|
35
|
+
// the consumer's functions/ directory). MUST be the line firebase-tools
|
|
36
|
+
// prints at the START of each reload cycle, not somewhere in the middle.
|
|
37
|
+
// If we rolled mid-cycle (e.g. on "Loaded functions definitions"), the
|
|
38
|
+
// tail of the reload sequence still gets captured into the fresh log;
|
|
39
|
+
// but firebase serve sometimes only emits the trailing function-initialized
|
|
40
|
+
// lines on the first cycle (subsequent cycles route those elsewhere), so
|
|
41
|
+
// we'd end up with a near-empty log. Rolling at the START of the cycle
|
|
42
|
+
// lets us capture whatever firebase-tools does emit, complete-or-not.
|
|
29
43
|
const logPath = path.join(projectDir, 'functions', 'serve.log');
|
|
30
|
-
const
|
|
44
|
+
const resetSentinelPath = `${logPath}.reset`;
|
|
45
|
+
// Match any node version: "Using node@22 from host.", "Using node@20 from host.", etc.
|
|
46
|
+
const RELOAD_MARKER = /Using node@\d+ from host\./;
|
|
31
47
|
const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
|
|
32
48
|
|
|
49
|
+
let currentStream = fs.createWriteStream(logPath, { flags: 'w' });
|
|
50
|
+
let reloadCount = 0; // skip rolling on the first marker (initial boot, not a reload)
|
|
51
|
+
|
|
52
|
+
function rollLog() {
|
|
53
|
+
try {
|
|
54
|
+
const oldStream = currentStream;
|
|
55
|
+
currentStream = fs.createWriteStream(logPath, { flags: 'w' });
|
|
56
|
+
oldStream.end();
|
|
57
|
+
} catch (e) {
|
|
58
|
+
// Best-effort. If roll fails, serve keeps running with the existing stream.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeToLog(data) {
|
|
63
|
+
if (currentStream && !currentStream.destroyed) {
|
|
64
|
+
currentStream.write(stripAnsi(data.toString()));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Clean up any stale sentinel from a prior crashed serve run
|
|
69
|
+
try { fs.unlinkSync(resetSentinelPath); } catch (e) { /* not present, ok */ }
|
|
70
|
+
|
|
71
|
+
// Poll every 500ms for the reset sentinel — cheap, no fs.watch quirks
|
|
72
|
+
const resetWatcher = setInterval(() => {
|
|
73
|
+
if (!fs.existsSync(resetSentinelPath)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
rollLog();
|
|
79
|
+
fs.unlinkSync(resetSentinelPath);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// Best-effort.
|
|
82
|
+
}
|
|
83
|
+
}, 500);
|
|
84
|
+
|
|
33
85
|
this.log(chalk.gray(` Logs saving to: ${logPath}\n`));
|
|
34
86
|
|
|
35
87
|
// Execute with tee to log file
|
|
@@ -42,21 +94,35 @@ class ServeCommand extends BaseCommand {
|
|
|
42
94
|
env: { ...process.env, FORCE_COLOR: '1' },
|
|
43
95
|
},
|
|
44
96
|
}, (child) => {
|
|
45
|
-
// Tee stdout to both console and log file (strip ANSI codes for clean log)
|
|
97
|
+
// Tee stdout to both console and log file (strip ANSI codes for clean log).
|
|
98
|
+
// Watch each chunk for the reload marker — when seen (after the initial boot),
|
|
99
|
+
// roll the log BEFORE writing this chunk so the marker becomes the first
|
|
100
|
+
// line of the fresh file.
|
|
46
101
|
child.stdout.on('data', (data) => {
|
|
47
102
|
process.stdout.write(data);
|
|
48
|
-
|
|
103
|
+
const text = data.toString();
|
|
104
|
+
if (RELOAD_MARKER.test(text)) {
|
|
105
|
+
reloadCount++;
|
|
106
|
+
if (reloadCount > 1) {
|
|
107
|
+
rollLog();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
writeToLog(data);
|
|
49
111
|
});
|
|
50
112
|
|
|
51
113
|
// Tee stderr to both console and log file (strip ANSI codes for clean log)
|
|
52
114
|
child.stderr.on('data', (data) => {
|
|
53
115
|
process.stderr.write(data);
|
|
54
|
-
|
|
116
|
+
writeToLog(data);
|
|
55
117
|
});
|
|
56
118
|
|
|
57
|
-
// Clean up log stream when child exits
|
|
119
|
+
// Clean up log stream + watcher when child exits
|
|
58
120
|
child.on('close', () => {
|
|
59
|
-
|
|
121
|
+
clearInterval(resetWatcher);
|
|
122
|
+
if (currentStream && !currentStream.destroyed) {
|
|
123
|
+
currentStream.end();
|
|
124
|
+
}
|
|
125
|
+
try { fs.unlinkSync(resetSentinelPath); } catch (e) { /* ok */ }
|
|
60
126
|
});
|
|
61
127
|
});
|
|
62
128
|
} catch (error) {
|
package/src/cli/commands/test.js
CHANGED
|
@@ -51,7 +51,7 @@ class TestCommand extends BaseCommand {
|
|
|
51
51
|
// Use hosting URL for all API requests (rewrites to bm_api function)
|
|
52
52
|
const testConfig = {
|
|
53
53
|
...projectConfig,
|
|
54
|
-
|
|
54
|
+
apiUrl: `http://127.0.0.1:${emulatorPorts.hosting}`,
|
|
55
55
|
projectDir,
|
|
56
56
|
testPaths,
|
|
57
57
|
emulatorPorts,
|
|
@@ -185,12 +185,58 @@ class TestCommand extends BaseCommand {
|
|
|
185
185
|
return this.isPortInUse(emulatorPorts.functions);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Signal the running emulator process to roll emulator.log.
|
|
190
|
+
*
|
|
191
|
+
* Mechanism: write a sentinel file at emulator.log.reset. The emulator command
|
|
192
|
+
* (src/cli/commands/emulator.js) polls for it and, on detection, closes its
|
|
193
|
+
* current write stream and reopens with flags: 'w' (truncating cleanly from its
|
|
194
|
+
* own perspective — avoids the sparse-file problem caused by external truncation).
|
|
195
|
+
*
|
|
196
|
+
* Waits up to 2s for the sentinel to be consumed. If it's still there after 2s
|
|
197
|
+
* the emulator isn't watching (probably running an older BEM or started outside
|
|
198
|
+
* `npx mgr emulator`); we delete the sentinel and proceed — tests still run, the
|
|
199
|
+
* log just won't be reset for this run.
|
|
200
|
+
*/
|
|
201
|
+
async requestEmulatorLogReset(projectDir) {
|
|
202
|
+
const logPath = path.join(projectDir, 'functions', 'emulator.log');
|
|
203
|
+
const sentinelPath = `${logPath}.reset`;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
fs.writeFileSync(sentinelPath, '');
|
|
207
|
+
} catch (e) {
|
|
208
|
+
return; // Can't write — skip, not fatal
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Poll for the emulator to consume the sentinel (it deletes the file when done)
|
|
212
|
+
const maxWaitMs = 2000;
|
|
213
|
+
const pollIntervalMs = 100;
|
|
214
|
+
const start = Date.now();
|
|
215
|
+
|
|
216
|
+
while (Date.now() - start < maxWaitMs) {
|
|
217
|
+
if (!fs.existsSync(sentinelPath)) {
|
|
218
|
+
return; // Emulator picked it up and rolled the log
|
|
219
|
+
}
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Timed out — emulator didn't see the sentinel. Clean up so we don't leave it behind.
|
|
224
|
+
try { fs.unlinkSync(sentinelPath); } catch (e) { /* ok */ }
|
|
225
|
+
}
|
|
226
|
+
|
|
188
227
|
/**
|
|
189
228
|
* Run tests directly (emulator already running)
|
|
190
229
|
*/
|
|
191
230
|
async runTestsDirectly(testCommand, functionsDir, emulatorPorts) {
|
|
192
231
|
const projectDir = this.main.firebaseProjectPath;
|
|
193
232
|
|
|
233
|
+
// Ask the running emulator process to roll emulator.log so this test run gets a
|
|
234
|
+
// clean slate. We touch a sentinel file the emulator polls for (every ~500ms) and
|
|
235
|
+
// wait briefly for it to be consumed. If the emulator isn't watching (older BEM
|
|
236
|
+
// version, or not started via `npx mgr emulator`), we time out silently — the log
|
|
237
|
+
// just won't be fresh, tests still run normally.
|
|
238
|
+
await this.requestEmulatorLogReset(projectDir);
|
|
239
|
+
|
|
194
240
|
this.log(chalk.gray(` Hosting: http://127.0.0.1:${emulatorPorts.hosting}`));
|
|
195
241
|
this.log(chalk.gray(` Firestore: 127.0.0.1:${emulatorPorts.firestore}`));
|
|
196
242
|
this.log(chalk.gray(` Auth: 127.0.0.1:${emulatorPorts.auth}`));
|
|
@@ -53,13 +53,21 @@ class WatchCommand extends BaseCommand {
|
|
|
53
53
|
// So we must: 1) ensure file exists, 2) wait for FS to settle, 3) write new content
|
|
54
54
|
// --on-change-only: only run exec when files change, not on initial startup
|
|
55
55
|
// --delay 1: debounce multiple rapid changes into one trigger
|
|
56
|
+
//
|
|
57
|
+
// The exec also drops <log>.reset sentinels so the parent serve/emulator command
|
|
58
|
+
// rolls its log file cleanly on every hot reload (mirrors the emulator log-roll
|
|
59
|
+
// pattern used by the test runner). Sentinels are best-effort — if a parent
|
|
60
|
+
// command isn't watching, the file is harmless and gets cleaned up by the next
|
|
61
|
+
// boot's stale-sentinel sweep.
|
|
56
62
|
const triggerFile = config.triggerFile;
|
|
63
|
+
const serveLogResetPath = path.join(config.functionsDir, 'serve.log.reset');
|
|
64
|
+
const emulatorLogResetPath = path.join(config.functionsDir, 'emulator.log.reset');
|
|
57
65
|
const nodemon = spawn(nodemonPath, [
|
|
58
66
|
'--on-change-only',
|
|
59
67
|
'--delay', '1',
|
|
60
68
|
'--watch', config.bemSrcDir,
|
|
61
69
|
'--ext', 'js,json',
|
|
62
|
-
'--exec', `node -e "var f='${triggerFile}',fs=require('fs');if(!fs.existsSync(f)){fs.writeFileSync(f,'// init');require('child_process').execSync('sleep 0.1');}fs.writeFileSync(f,'// '+Date.now())" && echo " [BEM] Triggered hot reload"`,
|
|
70
|
+
'--exec', `node -e "var f='${triggerFile}',fs=require('fs');if(!fs.existsSync(f)){fs.writeFileSync(f,'// init');require('child_process').execSync('sleep 0.1');}fs.writeFileSync(f,'// '+Date.now());try{fs.writeFileSync('${serveLogResetPath}','');}catch(e){}try{fs.writeFileSync('${emulatorLogResetPath}','');}catch(e){}" && echo " [BEM] Triggered hot reload"`,
|
|
63
71
|
], {
|
|
64
72
|
stdio: 'inherit',
|
|
65
73
|
detached: false,
|
|
@@ -93,11 +101,15 @@ class WatchCommand extends BaseCommand {
|
|
|
93
101
|
jetpack.write(config.triggerFile, `// BEM reload trigger\n`);
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
// Use nodemon to watch the BEM src directory and touch the trigger file on changes
|
|
104
|
+
// Use nodemon to watch the BEM src directory and touch the trigger file on changes.
|
|
105
|
+
// Also drop <log>.reset sentinels so any sibling serve/emulator command rolls its
|
|
106
|
+
// log file on each reload (mirrors the test runner's log-roll pattern).
|
|
107
|
+
const serveLogResetPath = path.join(config.functionsDir, 'serve.log.reset');
|
|
108
|
+
const emulatorLogResetPath = path.join(config.functionsDir, 'emulator.log.reset');
|
|
97
109
|
const nodemon = spawn(nodemonPath, [
|
|
98
110
|
'--watch', config.bemSrcDir,
|
|
99
111
|
'--ext', 'js,json',
|
|
100
|
-
'--exec', `touch "${config.triggerFile}" && echo " → Triggered hot reload"`,
|
|
112
|
+
'--exec', `touch "${config.triggerFile}" "${serveLogResetPath}" "${emulatorLogResetPath}" && echo " → Triggered hot reload"`,
|
|
101
113
|
], {
|
|
102
114
|
stdio: 'inherit',
|
|
103
115
|
cwd: config.bemDir,
|
|
@@ -147,6 +147,35 @@ const SCHEMA = {
|
|
|
147
147
|
page: { type: 'string', default: null, nullable: true },
|
|
148
148
|
},
|
|
149
149
|
},
|
|
150
|
+
consent: {
|
|
151
|
+
legal: {
|
|
152
|
+
status: { type: 'string', default: 'revoked' },
|
|
153
|
+
grantedAt: {
|
|
154
|
+
timestamp: { type: 'string', default: null, nullable: true },
|
|
155
|
+
timestampUNIX: { type: 'number', default: null, nullable: true },
|
|
156
|
+
source: { type: 'string', default: null, nullable: true },
|
|
157
|
+
ip: { type: 'string', default: null, nullable: true },
|
|
158
|
+
text: { type: 'string', default: null, nullable: true },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
marketing: {
|
|
162
|
+
status: { type: 'string', default: 'revoked' },
|
|
163
|
+
grantedAt: {
|
|
164
|
+
timestamp: { type: 'string', default: null, nullable: true },
|
|
165
|
+
timestampUNIX: { type: 'number', default: null, nullable: true },
|
|
166
|
+
source: { type: 'string', default: null, nullable: true },
|
|
167
|
+
ip: { type: 'string', default: null, nullable: true },
|
|
168
|
+
text: { type: 'string', default: null, nullable: true },
|
|
169
|
+
},
|
|
170
|
+
revokedAt: {
|
|
171
|
+
timestamp: { type: 'string', default: null, nullable: true },
|
|
172
|
+
timestampUNIX: { type: 'number', default: null, nullable: true },
|
|
173
|
+
source: { type: 'string', default: null, nullable: true },
|
|
174
|
+
ip: { type: 'string', default: null, nullable: true },
|
|
175
|
+
text: { type: 'string', default: null, nullable: true },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
150
179
|
};
|
|
151
180
|
|
|
152
181
|
/**
|
package/src/manager/index.js
CHANGED
|
@@ -210,6 +210,35 @@ Manager.prototype.init = function (exporter, options) {
|
|
|
210
210
|
: self.config.brand?.url || '';
|
|
211
211
|
};
|
|
212
212
|
|
|
213
|
+
// Resolve the parent BEM's website URL (the parent's brand domain, NO `api.` subdomain).
|
|
214
|
+
// - If config.parent === 'self', THIS BEM is the parent — returns this brand's own URL.
|
|
215
|
+
// - If config.parent is a URL, returns it as-is.
|
|
216
|
+
// - Returns '' if neither is configured.
|
|
217
|
+
// Use getParentApiUrl() for the API URL (with `api.` subdomain inserted).
|
|
218
|
+
self.getParentUrl = function() {
|
|
219
|
+
const parent = self.config.parent;
|
|
220
|
+
if (parent === 'self') {
|
|
221
|
+
return self.config.brand?.url || '';
|
|
222
|
+
}
|
|
223
|
+
return parent || '';
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Resolve the parent BEM's API URL (`https://api.{parent-host}`).
|
|
227
|
+
// ALWAYS returns the live production URL — even when THIS brand is running
|
|
228
|
+
// in dev/test mode. The parent's API is a real remote server (no localhost
|
|
229
|
+
// equivalent), so dev-mode does NOT redirect to localhost the way getApiUrl()
|
|
230
|
+
// does. Use this when you need to call the parent's API from any environment.
|
|
231
|
+
self.getParentApiUrl = function() {
|
|
232
|
+
const base = self.getParentUrl().replace(/^https?:\/\//, '');
|
|
233
|
+
return base ? `https://api.${base}` : '';
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Returns true when this BEM IS the parent (config.parent === 'self').
|
|
237
|
+
// Gates parent-only routes like /marketing/webhook/forward.
|
|
238
|
+
self.isParent = function() {
|
|
239
|
+
return self.config.parent === 'self';
|
|
240
|
+
};
|
|
241
|
+
|
|
213
242
|
// Set more properties (need to wait for assistant to determine if DEV)
|
|
214
243
|
self.project.functionsUrl = self.getFunctionsUrl();
|
|
215
244
|
|
|
@@ -242,6 +242,7 @@
|
|
|
242
242
|
"ac20mail.in",
|
|
243
243
|
"academiccommunity.com",
|
|
244
244
|
"academywe.us",
|
|
245
|
+
"acanok.com",
|
|
245
246
|
"acc1s.com",
|
|
246
247
|
"acc1s.net",
|
|
247
248
|
"accclone.com",
|
|
@@ -653,6 +654,7 @@
|
|
|
653
654
|
"biojuris.com",
|
|
654
655
|
"bione.co",
|
|
655
656
|
"bipochub.com",
|
|
657
|
+
"bitmah.com",
|
|
656
658
|
"bitmens.com",
|
|
657
659
|
"bitwhites.top",
|
|
658
660
|
"bitymails.us",
|
|
@@ -1072,6 +1074,7 @@
|
|
|
1072
1074
|
"dao.pp.ua",
|
|
1073
1075
|
"daouse.com",
|
|
1074
1076
|
"dapurx.me",
|
|
1077
|
+
"dardr.com",
|
|
1075
1078
|
"darkharvestfilms.com",
|
|
1076
1079
|
"daryxfox.net",
|
|
1077
1080
|
"dasdasdascyka.tk",
|
|
@@ -1937,6 +1940,7 @@
|
|
|
1937
1940
|
"getairmail.gq",
|
|
1938
1941
|
"getairmail.ml",
|
|
1939
1942
|
"getairmail.tk",
|
|
1943
|
+
"getasail.com",
|
|
1940
1944
|
"geteit.com",
|
|
1941
1945
|
"getfun.men",
|
|
1942
1946
|
"getmail1.com",
|
|
@@ -2164,6 +2168,7 @@
|
|
|
2164
2168
|
"hidemail.pro",
|
|
2165
2169
|
"hidemail.us",
|
|
2166
2170
|
"hidesmail.net",
|
|
2171
|
+
"hidevak.com",
|
|
2167
2172
|
"hidingmail.net",
|
|
2168
2173
|
"hidmail.org",
|
|
2169
2174
|
"hidzz.com",
|
|
@@ -2173,6 +2178,7 @@
|
|
|
2173
2178
|
"highstar.shop",
|
|
2174
2179
|
"hihi.lol",
|
|
2175
2180
|
"hikuhu.com",
|
|
2181
|
+
"hilostar.com",
|
|
2176
2182
|
"hiltonvr.com",
|
|
2177
2183
|
"him6.com",
|
|
2178
2184
|
"himail.infos.st",
|
|
@@ -2190,6 +2196,7 @@
|
|
|
2190
2196
|
"holio.day",
|
|
2191
2197
|
"holl.ga",
|
|
2192
2198
|
"homecut.pro",
|
|
2199
|
+
"homvela.com",
|
|
2193
2200
|
"honesthirianinda.net",
|
|
2194
2201
|
"honeys.be",
|
|
2195
2202
|
"honor-8.com",
|
|
@@ -2434,6 +2441,7 @@
|
|
|
2434
2441
|
"itcompu.com",
|
|
2435
2442
|
"itfast.net",
|
|
2436
2443
|
"itmo.edu.pl",
|
|
2444
|
+
"itquoted.com",
|
|
2437
2445
|
"itsbds.com",
|
|
2438
2446
|
"itsedit.click",
|
|
2439
2447
|
"itsjiff.com",
|
|
@@ -77,7 +77,7 @@ async function generate(Manager, assistant, settings, opts = {}) {
|
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const parentUrl = Manager.
|
|
80
|
+
const parentUrl = Manager.getParentApiUrl();
|
|
81
81
|
|
|
82
82
|
if (!parentUrl) {
|
|
83
83
|
assistant.log('Newsletter generator: no parent URL configured');
|
|
@@ -311,7 +311,7 @@ async function generate(Manager, assistant, settings, opts = {}) {
|
|
|
311
311
|
|
|
312
312
|
// 4. Mark sources as used on parent server (unless caller opted out)
|
|
313
313
|
if (!opts.skipClaim) {
|
|
314
|
-
const parentUrl = Manager.
|
|
314
|
+
const parentUrl = Manager.getParentApiUrl();
|
|
315
315
|
|
|
316
316
|
if (parentUrl) {
|
|
317
317
|
await claimSources(parentUrl, sources, brand?.id, assistant);
|
|
@@ -452,6 +452,18 @@ function resolveProduct(raw, config) {
|
|
|
452
452
|
return { id: 'basic', name: 'Basic' };
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
+
// Test-mode sentinel: the test processor synthesizes "_test_<id>" when no real
|
|
456
|
+
// Stripe product is configured. Map it back to the matching BEM product so the
|
|
457
|
+
// pipeline can be exercised end-to-end without real Stripe credentials.
|
|
458
|
+
if (typeof stripeProductId === 'string' && stripeProductId.startsWith('_test_')) {
|
|
459
|
+
const bemId = stripeProductId.slice('_test_'.length);
|
|
460
|
+
const product = config.payment.products.find((p) => p.id === bemId);
|
|
461
|
+
if (product) {
|
|
462
|
+
return { id: product.id, name: product.name || product.id };
|
|
463
|
+
}
|
|
464
|
+
return { id: 'basic', name: 'Basic' };
|
|
465
|
+
}
|
|
466
|
+
|
|
455
467
|
for (const product of config.payment.products) {
|
|
456
468
|
// Match current product ID
|
|
457
469
|
if (product.stripe?.productId === stripeProductId) {
|
|
@@ -174,5 +174,12 @@ function resolveStripeProductId(productId, config) {
|
|
|
174
174
|
|
|
175
175
|
const product = config.payment.products.find(p => p.id === productId);
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
if (!product) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Real Stripe product ID if configured, otherwise the "_test_<id>" sentinel that the
|
|
182
|
+
// Stripe resolver recognizes and maps back to the BEM product. Lets reconstruction
|
|
183
|
+
// work in brands without real Stripe (Somiibo uses PayPal, Chargebee, etc.).
|
|
184
|
+
return product.stripe?.productId || `_test_${product.id}`;
|
|
178
185
|
}
|
|
@@ -16,8 +16,9 @@ module.exports = async ({ assistant, user, settings }) => {
|
|
|
16
16
|
return assistant.respond('Admin required.', { code: 403 });
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
// Accept single email or array of emails
|
|
20
|
-
|
|
19
|
+
// Accept single email or array of emails. Schema defaults `emails` to [], so check length:
|
|
20
|
+
// an empty default should NOT shadow a provided single `email`.
|
|
21
|
+
const emails = Array.isArray(settings.emails) && settings.emails.length > 0
|
|
21
22
|
? settings.emails
|
|
22
23
|
: [settings.email];
|
|
23
24
|
|