backend-manager 5.2.19 → 5.3.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/CHANGELOG.md +19 -0
- package/CLAUDE.md +5 -4
- package/docs/admin-post-route.md +2 -0
- package/docs/ai-library.md +29 -2
- package/docs/common-mistakes.md +1 -1
- package/docs/environment-detection.md +87 -3
- package/docs/marketing-campaigns.md +1 -1
- package/docs/payment-system.md +1 -1
- package/docs/testing.md +60 -15
- package/package.json +1 -1
- package/src/cli/commands/install.js +2 -2
- package/src/cli/commands/setup.js +12 -4
- package/src/cli/commands/test.js +1 -1
- package/src/cli/index.js +6 -2
- package/src/defaults/CLAUDE.md +2 -2
- package/src/defaults/test/_init.js +14 -0
- package/src/manager/functions/_legacy/actions/create-post-handler.js +1 -1
- package/src/manager/functions/core/actions/api/admin/edit-post.js +1 -1
- package/src/manager/functions/core/actions/api/general/send-email.js +1 -1
- package/src/manager/functions/core/actions/api/user/delete.js +1 -1
- package/src/manager/functions/core/actions/api/user/oauth2.js +1 -1
- package/src/manager/helpers/analytics.js +3 -1
- package/src/manager/helpers/assistant.js +43 -38
- package/src/manager/index.js +76 -12
- package/src/manager/libraries/ai/index.js +33 -0
- package/src/manager/libraries/ai/providers/openai.js +105 -8
- package/src/manager/libraries/content/ghostii.js +76 -16
- package/src/manager/libraries/email/data/disposable-domains.json +23 -0
- package/src/manager/libraries/email/generators/lib/image-illustrator.js +154 -0
- package/src/manager/libraries/email/generators/newsletter.js +16 -2
- package/src/manager/libraries/payment/discount-codes.js +1 -0
- package/src/manager/routes/admin/post/put.js +1 -1
- package/src/manager/routes/handler/post/post.js +1 -1
- package/src/manager/routes/payments/intent/processors/test.js +1 -1
- package/src/manager/routes/user/delete.js +1 -1
- package/src/manager/routes/user/signup/post.js +4 -2
- package/src/test/runner.js +142 -20
- package/src/test/test-accounts.js +159 -102
- package/test/events/payments/journey-payments-cancel-endpoint.js +6 -2
- package/test/events/payments/journey-payments-cancel.js +6 -3
- package/test/events/payments/journey-payments-failure.js +6 -3
- package/test/events/payments/journey-payments-plan-change.js +6 -3
- package/test/events/payments/journey-payments-refund-webhook.js +6 -2
- package/test/events/payments/journey-payments-suspend.js +6 -3
- package/test/events/payments/journey-payments-trial-cancel.js +6 -3
- package/test/events/payments/journey-payments-trial.js +10 -4
- package/test/events/payments/journey-payments-uid-resolution.js +6 -2
- package/test/events/payments/journey-payments-upgrade.js +6 -3
- package/test/helpers/content/ghostii-blocks.js +134 -0
- package/test/helpers/environment.js +230 -0
- package/test/routes/marketing/webhook-forward.js +14 -7
- package/test/routes/payments/cancel.js +4 -1
- package/test/routes/payments/dispute-alert.js +9 -6
- package/test/routes/payments/intent.js +55 -14
- package/test/routes/payments/portal.js +4 -1
- package/test/routes/payments/refund.js +4 -1
|
@@ -602,6 +602,7 @@
|
|
|
602
602
|
"beaconmessenger.com",
|
|
603
603
|
"bearsarefuzzy.com",
|
|
604
604
|
"beastmail.space",
|
|
605
|
+
"beatbok.online",
|
|
605
606
|
"beddly.com",
|
|
606
607
|
"beefmilk.com",
|
|
607
608
|
"beelsil.com",
|
|
@@ -1833,6 +1834,7 @@
|
|
|
1833
1834
|
"freeteenbums.com",
|
|
1834
1835
|
"freetimer.online",
|
|
1835
1836
|
"freundin.ru",
|
|
1837
|
+
"friday12.store",
|
|
1836
1838
|
"friendlymail.co.uk",
|
|
1837
1839
|
"frm.ovh",
|
|
1838
1840
|
"front14.org",
|
|
@@ -2058,6 +2060,7 @@
|
|
|
2058
2060
|
"gowikitv.com",
|
|
2059
2061
|
"grandmamail.com",
|
|
2060
2062
|
"grandmasmail.com",
|
|
2063
|
+
"graphicbulk.site",
|
|
2061
2064
|
"graphicenda.site",
|
|
2062
2065
|
"grassdev.com",
|
|
2063
2066
|
"gravityengine.cc",
|
|
@@ -2128,6 +2131,7 @@
|
|
|
2128
2131
|
"haltospam.com",
|
|
2129
2132
|
"hamham.uk",
|
|
2130
2133
|
"handrik.com",
|
|
2134
|
+
"hanghai232.shop",
|
|
2131
2135
|
"hangxomcuatoilatotoro.ml",
|
|
2132
2136
|
"happiseektest.com",
|
|
2133
2137
|
"happy-new-year.top",
|
|
@@ -2238,6 +2242,7 @@
|
|
|
2238
2242
|
"housereformas.es",
|
|
2239
2243
|
"hpari.com",
|
|
2240
2244
|
"hpc.tw",
|
|
2245
|
+
"hqpdf.com",
|
|
2241
2246
|
"hs.vc",
|
|
2242
2247
|
"hsmw.net",
|
|
2243
2248
|
"ht.cx",
|
|
@@ -2257,6 +2262,7 @@
|
|
|
2257
2262
|
"hushmail.cf",
|
|
2258
2263
|
"huskion.net",
|
|
2259
2264
|
"hutudns.com",
|
|
2265
|
+
"huytuyen.click",
|
|
2260
2266
|
"hvastudiesucces.nl",
|
|
2261
2267
|
"hwsye.net",
|
|
2262
2268
|
"hxopi.ru",
|
|
@@ -2489,6 +2495,7 @@
|
|
|
2489
2495
|
"jdmadventures.com",
|
|
2490
2496
|
"jdz.ro",
|
|
2491
2497
|
"je-recycle.info",
|
|
2498
|
+
"jejes.de",
|
|
2492
2499
|
"jellow.ml",
|
|
2493
2500
|
"jellyrolls.com",
|
|
2494
2501
|
"jeoce.com",
|
|
@@ -2714,6 +2721,7 @@
|
|
|
2714
2721
|
"laste.ml",
|
|
2715
2722
|
"lastmail.co",
|
|
2716
2723
|
"lastmail.com",
|
|
2724
|
+
"laulund.info",
|
|
2717
2725
|
"launders.money",
|
|
2718
2726
|
"lawlita.com",
|
|
2719
2727
|
"laxex.ru",
|
|
@@ -3163,6 +3171,7 @@
|
|
|
3163
3171
|
"midlertidig.com",
|
|
3164
3172
|
"midlertidig.net",
|
|
3165
3173
|
"midlertidig.org",
|
|
3174
|
+
"midwestpride.org",
|
|
3166
3175
|
"mierdamail.com",
|
|
3167
3176
|
"migmail.net",
|
|
3168
3177
|
"migmail.pl",
|
|
@@ -3376,6 +3385,7 @@
|
|
|
3376
3385
|
"nanonym.ch",
|
|
3377
3386
|
"nanopools.info",
|
|
3378
3387
|
"naobk.com",
|
|
3388
|
+
"narrowment.com",
|
|
3379
3389
|
"narsub.online",
|
|
3380
3390
|
"narsub.shop",
|
|
3381
3391
|
"naslazhdai.ru",
|
|
@@ -3542,6 +3552,7 @@
|
|
|
3542
3552
|
"nutpa.net",
|
|
3543
3553
|
"nuts2trade.com",
|
|
3544
3554
|
"nvhrw.com",
|
|
3555
|
+
"nvqt304.xyz",
|
|
3545
3556
|
"nwldx.com",
|
|
3546
3557
|
"nwytg.com",
|
|
3547
3558
|
"nwytg.net",
|
|
@@ -3584,6 +3595,7 @@
|
|
|
3584
3595
|
"oku.ovh",
|
|
3585
3596
|
"oky.ovh",
|
|
3586
3597
|
"okzk.com",
|
|
3598
|
+
"oldao.com",
|
|
3587
3599
|
"olimp-case.ru",
|
|
3588
3600
|
"oliviadiffuser.store",
|
|
3589
3601
|
"oloh.ru",
|
|
@@ -3594,6 +3606,7 @@
|
|
|
3594
3606
|
"omarnasrrr.com",
|
|
3595
3607
|
"omfg.run",
|
|
3596
3608
|
"omnievents.org",
|
|
3609
|
+
"omnilog.space",
|
|
3597
3610
|
"omtecha.com",
|
|
3598
3611
|
"onbap.com",
|
|
3599
3612
|
"one-mail.top",
|
|
@@ -3822,6 +3835,7 @@
|
|
|
3822
3835
|
"powerscrews.com",
|
|
3823
3836
|
"powlearn.com",
|
|
3824
3837
|
"pox2.com",
|
|
3838
|
+
"pozima.rest",
|
|
3825
3839
|
"pp7rvv.com",
|
|
3826
3840
|
"ppetw.com",
|
|
3827
3841
|
"pptrvv.com",
|
|
@@ -4060,6 +4074,7 @@
|
|
|
4060
4074
|
"royaldoodles.org",
|
|
4061
4075
|
"royalmarket.life",
|
|
4062
4076
|
"royandk.com",
|
|
4077
|
+
"roybl.com",
|
|
4063
4078
|
"rppkn.com",
|
|
4064
4079
|
"rr.nu",
|
|
4065
4080
|
"rsvhr.com",
|
|
@@ -4069,6 +4084,7 @@
|
|
|
4069
4084
|
"rudymail.ml",
|
|
4070
4085
|
"rulersonline.com",
|
|
4071
4086
|
"rumgel.com",
|
|
4087
|
+
"run.place",
|
|
4072
4088
|
"runi.ca",
|
|
4073
4089
|
"rupayamail.com",
|
|
4074
4090
|
"ruru.be",
|
|
@@ -4097,6 +4113,7 @@
|
|
|
4097
4113
|
"sahildash.dev",
|
|
4098
4114
|
"sailmail.io",
|
|
4099
4115
|
"sajutadollars.com",
|
|
4116
|
+
"salakoais.shop",
|
|
4100
4117
|
"saleis.live",
|
|
4101
4118
|
"salmeow.tk",
|
|
4102
4119
|
"sam1.eu.org",
|
|
@@ -4419,6 +4436,7 @@
|
|
|
4419
4436
|
"spymail.one",
|
|
4420
4437
|
"squizzy.de",
|
|
4421
4438
|
"squizzy.net",
|
|
4439
|
+
"srame.com",
|
|
4422
4440
|
"sroff.com",
|
|
4423
4441
|
"sry.li",
|
|
4424
4442
|
"ssi-bsn.infos.st",
|
|
@@ -4816,6 +4834,7 @@
|
|
|
4816
4834
|
"trackden.com",
|
|
4817
4835
|
"tradermail.info",
|
|
4818
4836
|
"tranceversal.com",
|
|
4837
|
+
"trandigabapoe.shop",
|
|
4819
4838
|
"trangiabao.click",
|
|
4820
4839
|
"trangzim.uk",
|
|
4821
4840
|
"translateid.com",
|
|
@@ -5287,6 +5306,7 @@
|
|
|
5287
5306
|
"wyoxafp.com",
|
|
5288
5307
|
"wywnxa.com",
|
|
5289
5308
|
"wzofit.com",
|
|
5309
|
+
"x-clone.asia",
|
|
5290
5310
|
"x-lab.net",
|
|
5291
5311
|
"x0q.net",
|
|
5292
5312
|
"x1ix.com",
|
|
@@ -5295,6 +5315,8 @@
|
|
|
5295
5315
|
"xagloo.com",
|
|
5296
5316
|
"xapimail.top",
|
|
5297
5317
|
"xbaby69.top",
|
|
5318
|
+
"xbrok.com",
|
|
5319
|
+
"xclone.asia",
|
|
5298
5320
|
"xcode.ro",
|
|
5299
5321
|
"xcodes.net",
|
|
5300
5322
|
"xcompress.com",
|
|
@@ -5365,6 +5387,7 @@
|
|
|
5365
5387
|
"yeezus.ru",
|
|
5366
5388
|
"yep.it",
|
|
5367
5389
|
"yermail.net",
|
|
5390
|
+
"yeummo.io.vn",
|
|
5368
5391
|
"yhg.biz",
|
|
5369
5392
|
"ynmrealty.com",
|
|
5370
5393
|
"yodx.ro",
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image illustrator — AI generates a flat-vector PNG illustration per section
|
|
3
|
+
* directly via OpenAI's image model (gpt-image-2), no SVG-author-then-rasterize step.
|
|
4
|
+
*
|
|
5
|
+
* This is the DEFAULT newsletter illustration method. It replaces the older
|
|
6
|
+
* svg-illustrator.js approach (AI writes an <svg>, resvg rasterizes it) which is
|
|
7
|
+
* still available as a fallback via newsletterConfig.method.image = 'svg'.
|
|
8
|
+
*
|
|
9
|
+
* Output matches svg-illustrator's contract: { png: Buffer, fallback, meta }.
|
|
10
|
+
* The newsletter pipeline (image-host.js) only requires `img.png` to be a PNG Buffer.
|
|
11
|
+
*
|
|
12
|
+
* Style: clean flat 2D vector illustration (Stripe / Linear / undraw.co aesthetic),
|
|
13
|
+
* built from the brand palette, on a white background, no text. The prompt mirrors
|
|
14
|
+
* the validated spike in scripts/.temp/ai-image-test/.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_MODEL = 'gpt-image-2';
|
|
17
|
+
const DEFAULT_SIZE = '1024x1024'; // square reads best for flat section art
|
|
18
|
+
const DEFAULT_QUALITY = 'medium'; // medium is plenty for flat vector; ~40-50s/image
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate one illustration for a section.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} args
|
|
24
|
+
* @param {string} args.imagePrompt - Visual description from the structure
|
|
25
|
+
* @param {object} args.brand - { name, tagline, color: { primary, secondary, ... } }
|
|
26
|
+
* @param {object} args.newsletterConfig - marketing.beehiiv.content
|
|
27
|
+
* @param {object} args.ai - Manager.AI() instance
|
|
28
|
+
* @param {object} args.assistant - BEM assistant
|
|
29
|
+
* @returns {Promise<{png: Buffer, fallback: boolean, meta: object}>}
|
|
30
|
+
*/
|
|
31
|
+
async function generateSectionImage({ imagePrompt, brand, newsletterConfig, ai, assistant }) {
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
const model = newsletterConfig?.model?.image || DEFAULT_MODEL;
|
|
34
|
+
const size = newsletterConfig?.image?.size || DEFAULT_SIZE;
|
|
35
|
+
const quality = newsletterConfig?.image?.quality || DEFAULT_QUALITY;
|
|
36
|
+
|
|
37
|
+
const palette = resolvePalette(brand, newsletterConfig);
|
|
38
|
+
const subject = imagePrompt || 'An abstract geometric shape representing the topic.';
|
|
39
|
+
const prompt = buildImagePrompt({ brand, palette, subject });
|
|
40
|
+
|
|
41
|
+
let png = null;
|
|
42
|
+
let fallback = false;
|
|
43
|
+
let revisedPrompt = null;
|
|
44
|
+
let attempts = 0;
|
|
45
|
+
|
|
46
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
47
|
+
attempts++;
|
|
48
|
+
try {
|
|
49
|
+
const result = await ai.image({
|
|
50
|
+
provider: 'openai',
|
|
51
|
+
model,
|
|
52
|
+
prompt,
|
|
53
|
+
size,
|
|
54
|
+
quality,
|
|
55
|
+
background: 'opaque',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (result?.png || result?.buffer) {
|
|
59
|
+
png = result.png || result.buffer;
|
|
60
|
+
revisedPrompt = result.revisedPrompt || null;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
assistant.log(`Image generation attempt ${attempt + 1} returned no buffer`);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
assistant.error(`Image generation attempt ${attempt + 1} failed: ${e.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// No transparent-PNG placeholder fallback that's valid as an image — if both
|
|
71
|
+
// attempts fail, return a 1x1 transparent PNG so the pipeline doesn't crash on
|
|
72
|
+
// a non-Buffer. The caller logs and proceeds.
|
|
73
|
+
if (!png) {
|
|
74
|
+
png = TRANSPARENT_PNG;
|
|
75
|
+
fallback = true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
png,
|
|
80
|
+
fallback,
|
|
81
|
+
meta: {
|
|
82
|
+
method: 'image',
|
|
83
|
+
provider: 'openai',
|
|
84
|
+
model,
|
|
85
|
+
size,
|
|
86
|
+
quality,
|
|
87
|
+
durationMs: Date.now() - startTime,
|
|
88
|
+
attempts,
|
|
89
|
+
fallback,
|
|
90
|
+
revisedPrompt,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve brand palette — same precedence as svg-illustrator.js#resolvePalette:
|
|
97
|
+
* newsletter theme colors → brand.color.* → sensible defaults.
|
|
98
|
+
*/
|
|
99
|
+
function resolvePalette(brand, newsletterConfig) {
|
|
100
|
+
const theme = newsletterConfig?.theme || {};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
primary: theme.primaryColor || brand?.color?.primary || '#4B5BFF',
|
|
104
|
+
secondary: theme.secondaryColor || brand?.color?.secondary || '#1E1E2A',
|
|
105
|
+
accent: theme.accentColor || brand?.color?.accent || '#F4F6FA',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the flat-vector art-direction prompt with brand identity + palette baked in.
|
|
111
|
+
* Mirrors the validated prompt from scripts/.temp/ai-image-test/generate.js.
|
|
112
|
+
*/
|
|
113
|
+
function buildImagePrompt({ brand, palette, subject }) {
|
|
114
|
+
const { primary, secondary, accent } = palette;
|
|
115
|
+
const name = brand?.name || 'the brand';
|
|
116
|
+
const tagline = brand?.tagline ? ` — ${brand.tagline}` : '';
|
|
117
|
+
|
|
118
|
+
return [
|
|
119
|
+
`Minimal flat vector illustration for the newsletter of "${name}"${tagline}.`,
|
|
120
|
+
'',
|
|
121
|
+
'STYLE: modern flat 2D vector illustration, like the marketing graphics on',
|
|
122
|
+
'Stripe, Linear, Vercel, Notion, or undraw.co. Clean geometric shapes with',
|
|
123
|
+
'flat solid color fills and crisp edges. Simple, friendly, and minimal.',
|
|
124
|
+
'',
|
|
125
|
+
'ABSOLUTELY NOT: no photorealism, no 3D renders, no realistic textures, no',
|
|
126
|
+
'photography, no depth of field, no dramatic lighting, no film grain, no',
|
|
127
|
+
'shading or gradients beyond a single subtle flat tone shift, no glossy',
|
|
128
|
+
'reflections, no metallic surfaces. This must look hand-designed in a vector',
|
|
129
|
+
'tool (Figma/Illustrator), NOT rendered or photographed.',
|
|
130
|
+
'',
|
|
131
|
+
'PALETTE (use ONLY these flat colors, plus white):',
|
|
132
|
+
`- Primary: ${primary}`,
|
|
133
|
+
`- Secondary: ${secondary}`,
|
|
134
|
+
`- Accent: ${accent}`,
|
|
135
|
+
'Flat solid fills only. A clean white or very-light background.',
|
|
136
|
+
'',
|
|
137
|
+
'COMPOSITION: single clear simple subject, lots of clean negative space,',
|
|
138
|
+
'centered or rule-of-thirds, uncluttered, plenty of breathing room.',
|
|
139
|
+
'',
|
|
140
|
+
'HARD CONSTRAINTS: absolutely NO text, NO words, NO letters, NO numbers,',
|
|
141
|
+
'NO logos, NO watermarks, NO UI mockups, NO borders or frames.',
|
|
142
|
+
'',
|
|
143
|
+
`SUBJECT (the focal scene to illustrate): ${subject}`,
|
|
144
|
+
].join('\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 1x1 transparent PNG — last-resort fallback so a failed generation still yields a
|
|
148
|
+
// valid PNG Buffer instead of crashing image-host.js's Buffer check.
|
|
149
|
+
const TRANSPARENT_PNG = Buffer.from(
|
|
150
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
151
|
+
'base64'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
module.exports = { generateSectionImage };
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* 1. Read newsletter categories from Manager.config.marketing.beehiiv.content.categories
|
|
10
10
|
* 2. Fetch ready sources from parent server (atomic claim via claimFor=brandId)
|
|
11
11
|
* 3. structure.js → AI authors subject, preheader, intro, sections, signoff
|
|
12
|
-
* 4.
|
|
12
|
+
* 4. image-illustrator.js → AI generates one flat-vector PNG per section via
|
|
13
|
+
* gpt-image-2 (default). Set content.method.image = 'svg' to use the legacy
|
|
14
|
+
* svg-illustrator.js (AI writes an <svg>, resvg rasterizes it) instead.
|
|
13
15
|
* 5. mjml-template.js → compile MJML → email-safe HTML
|
|
14
16
|
* 6. Persist PNGs (caller-provided via opts.persistImage or default no-op)
|
|
15
17
|
* 7. Mark sources as used on parent server
|
|
@@ -22,8 +24,19 @@ const fetch = require('wonderful-fetch');
|
|
|
22
24
|
|
|
23
25
|
const { filterSources } = require('./lib/filter.js');
|
|
24
26
|
const { generateStructure } = require('./lib/structure.js');
|
|
25
|
-
const { generateSectionImage } = require('./lib/
|
|
27
|
+
const { generateSectionImage: generateImageSection } = require('./lib/image-illustrator.js');
|
|
28
|
+
const { generateSectionImage: generateSvgSection } = require('./lib/svg-illustrator.js');
|
|
26
29
|
const { renderNewsletter } = require('./lib/mjml-template.js');
|
|
30
|
+
|
|
31
|
+
// Default illustration method. 'image' = gpt-image-2 flat-vector PNGs (default).
|
|
32
|
+
// 'svg' = legacy AI-authored SVG rasterized via resvg. Selected per-brand via
|
|
33
|
+
// marketing.beehiiv.content.method.image.
|
|
34
|
+
const DEFAULT_IMAGE_METHOD = 'image';
|
|
35
|
+
|
|
36
|
+
function resolveSectionImageFn(newsletterConfig) {
|
|
37
|
+
const method = newsletterConfig?.method?.image || DEFAULT_IMAGE_METHOD;
|
|
38
|
+
return method === 'svg' ? generateSvgSection : generateImageSection;
|
|
39
|
+
}
|
|
27
40
|
const { renderMarkdown } = require('./lib/markdown-renderer.js');
|
|
28
41
|
const { uploadAssets, RAW_BASE } = require('./lib/image-host.js');
|
|
29
42
|
const { buildPublicConfig } = require('../../../routes/brand/get.js');
|
|
@@ -165,6 +178,7 @@ async function generate(Manager, assistant, settings, opts = {}) {
|
|
|
165
178
|
return;
|
|
166
179
|
}
|
|
167
180
|
|
|
181
|
+
const generateSectionImage = resolveSectionImageFn(config);
|
|
168
182
|
const images = await Promise.all(
|
|
169
183
|
structure.sections.map((s) => generateSectionImage({
|
|
170
184
|
imagePrompt: s.image_prompt,
|
|
@@ -15,6 +15,7 @@ const DISCOUNT_CODES = {
|
|
|
15
15
|
'WELCOME15': { percent: 15, duration: 'once' },
|
|
16
16
|
'SAVE10': { percent: 10, duration: 'once' },
|
|
17
17
|
'FLASH20': { percent: 20, duration: 'once' },
|
|
18
|
+
'GIFT15': { percent: 15, duration: 'once' },
|
|
18
19
|
|
|
19
20
|
// Email campaigns (used by recurring sale seeds — restricted by audience)
|
|
20
21
|
'UPGRADE15': {
|
|
@@ -91,7 +91,7 @@ async function fetchPost(assistant, url) {
|
|
|
91
91
|
const fetch = Manager.require('wonderful-fetch');
|
|
92
92
|
|
|
93
93
|
// Use NEW API format
|
|
94
|
-
const result = await fetch(`${Manager.
|
|
94
|
+
const result = await fetch(`${Manager.getApiUrl()}/backend-manager/content/post`, {
|
|
95
95
|
method: 'get',
|
|
96
96
|
response: 'json',
|
|
97
97
|
timeout: 190000,
|
|
@@ -100,7 +100,7 @@ module.exports = async ({ assistant, Manager, user, settings, analytics }) => {
|
|
|
100
100
|
// Send notification (unless explicitly disabled)
|
|
101
101
|
if (settings.sendNotification !== false) {
|
|
102
102
|
// Use NEW API format
|
|
103
|
-
await fetch(`${Manager.
|
|
103
|
+
await fetch(`${Manager.getApiUrl()}/backend-manager/admin/notification`, {
|
|
104
104
|
method: 'POST',
|
|
105
105
|
response: 'json',
|
|
106
106
|
headers: {
|
|
@@ -155,7 +155,7 @@ async function createOneTimeIntent({ uid, orderId, product, productId, confirmat
|
|
|
155
155
|
* Fire-and-forget webhook to trigger the full pipeline
|
|
156
156
|
*/
|
|
157
157
|
function fireWebhook({ event, assistant }) {
|
|
158
|
-
const webhookUrl = `${assistant.Manager.
|
|
158
|
+
const webhookUrl = `${assistant.Manager.getApiUrl()}/backend-manager/payments/webhook?processor=test&key=${process.env.BACKEND_MANAGER_WEBHOOK_KEY}`;
|
|
159
159
|
fetch(webhookUrl, {
|
|
160
160
|
method: 'POST',
|
|
161
161
|
response: 'json',
|
|
@@ -46,7 +46,7 @@ module.exports = async ({ assistant, Manager, user, settings, libraries }) => {
|
|
|
46
46
|
// Sign out of all sessions first
|
|
47
47
|
assistant.log(`Signing out of all sessions for ${uid}...`);
|
|
48
48
|
|
|
49
|
-
await fetch(`${Manager.
|
|
49
|
+
await fetch(`${Manager.getApiUrl()}/backend-manager/user/sessions`, {
|
|
50
50
|
method: 'delete',
|
|
51
51
|
timeout: 60000,
|
|
52
52
|
response: 'json',
|
|
@@ -478,9 +478,11 @@ function sendDiscountNudgeEmail(assistant, uid, firstName) {
|
|
|
478
478
|
|
|
479
479
|
It's Ian, the founder of **${Manager.config.brand.name}**.
|
|
480
480
|
|
|
481
|
-
As a thank-you for giving us a try, I'd love to send you a code for a **premium upgrade**.
|
|
481
|
+
As a thank-you for giving us a try, I'd love to send you a code for a **premium upgrade**.
|
|
482
482
|
|
|
483
|
-
|
|
483
|
+
**Just reply to this email** and I'll get one over to you.
|
|
484
|
+
|
|
485
|
+
I read every reply and I'm looking forward to hearing from you!`,
|
|
484
486
|
},
|
|
485
487
|
signoff: {
|
|
486
488
|
type: 'personal',
|
package/src/test/runner.js
CHANGED
|
@@ -186,7 +186,7 @@ class TestRunner {
|
|
|
186
186
|
// the health endpoint re-reads the file as a freshness guard. By
|
|
187
187
|
// construction these are equal — no mismatch warning needed.
|
|
188
188
|
const emulatorExtended = !!response.data?.testExtendedMode;
|
|
189
|
-
console.log(chalk.gray(` Mode: ${emulatorExtended ? '
|
|
189
|
+
console.log(chalk.gray(` Mode: ${emulatorExtended ? 'extended (real external APIs)' : 'normal (external APIs skipped)'}`));
|
|
190
190
|
|
|
191
191
|
return true;
|
|
192
192
|
}
|
|
@@ -206,23 +206,32 @@ class TestRunner {
|
|
|
206
206
|
* Setup test accounts - deletes existing test users and recreates them fresh
|
|
207
207
|
*/
|
|
208
208
|
async setupAccounts() {
|
|
209
|
-
//
|
|
209
|
+
// Load the optional test/_init.js hooks from BOTH test roots (BEM core +
|
|
210
|
+
// consumer project): extra `accounts` to create and `setup()` to seed fixtures.
|
|
211
|
+
const initHooks = this.loadInitHooks();
|
|
212
|
+
|
|
213
|
+
// Flush the entire emulator Firestore + delete Auth test users (clean slate).
|
|
214
|
+
process.stdout.write(chalk.gray(' Wiping emulator + deleting test users... '));
|
|
215
|
+
const deleteResult = await testAccounts.deleteTestUsers(this.options.admin, initHooks.accounts);
|
|
216
|
+
console.log(chalk.green(`✓ (${deleteResult.deleted} deleted, ${deleteResult.skipped} skipped)`));
|
|
217
|
+
|
|
218
|
+
// Ensure meta/stats doc exists (required for on-create batch writes + the
|
|
219
|
+
// admin stats route). MUST run AFTER the wipe above — the flush recursively
|
|
220
|
+
// deletes every collection (including meta/stats), so seeding before it would
|
|
221
|
+
// be clobbered. Seeding here guarantees a `users` field survives even after
|
|
222
|
+
// the notification on-write trigger merges in its `notifications` counter.
|
|
210
223
|
process.stdout.write(chalk.gray(' Ensuring meta/stats doc exists... '));
|
|
211
224
|
await this.ensureMetaStats();
|
|
212
225
|
console.log(chalk.green('✓'));
|
|
213
226
|
|
|
214
|
-
// Delete existing test user documents to ensure clean state
|
|
215
|
-
process.stdout.write(chalk.gray(' Deleting existing test users... '));
|
|
216
|
-
const deleteResult = await testAccounts.deleteTestUsers(this.options.admin);
|
|
217
|
-
console.log(chalk.green(`✓ (${deleteResult.deleted} deleted, ${deleteResult.skipped} skipped)`));
|
|
218
|
-
|
|
219
227
|
process.stdout.write(chalk.gray(' Creating test accounts... '));
|
|
220
228
|
|
|
221
|
-
// Create fresh test accounts
|
|
229
|
+
// Create fresh test accounts (built-in + any project-defined via _init.js).
|
|
222
230
|
const result = await testAccounts.createTestAccounts(
|
|
223
231
|
this.options.admin,
|
|
224
232
|
this.options.domain,
|
|
225
|
-
this.config
|
|
233
|
+
this.config,
|
|
234
|
+
initHooks.accounts
|
|
226
235
|
);
|
|
227
236
|
|
|
228
237
|
if (!result.success) {
|
|
@@ -232,8 +241,28 @@ class TestRunner {
|
|
|
232
241
|
|
|
233
242
|
console.log(chalk.green(`✓ (${result.created} created)`));
|
|
234
243
|
|
|
235
|
-
// Fetch account privateKeys
|
|
236
|
-
this.accounts = await testAccounts.fetchPrivateKeys(this.options.admin, this.options.domain, this.config);
|
|
244
|
+
// Fetch account privateKeys (built-in + project-defined).
|
|
245
|
+
this.accounts = await testAccounts.fetchPrivateKeys(this.options.admin, this.options.domain, this.config, initHooks.accounts);
|
|
246
|
+
|
|
247
|
+
// Run custom setup hooks (BEM core first, then consumer). Runs AFTER the
|
|
248
|
+
// standard test accounts exist and AFTER the clean slate, so they can seed
|
|
249
|
+
// fixtures (brands, etc.) and reference the created accounts.
|
|
250
|
+
for (const setup of initHooks.setups) {
|
|
251
|
+
process.stdout.write(chalk.gray(' Running test/_init.js setup... '));
|
|
252
|
+
try {
|
|
253
|
+
await setup({
|
|
254
|
+
admin: this.options.admin,
|
|
255
|
+
config: this.config,
|
|
256
|
+
accounts: this.accounts,
|
|
257
|
+
Manager: this.config.Manager,
|
|
258
|
+
assistant: this.config.assistant,
|
|
259
|
+
});
|
|
260
|
+
console.log(chalk.green('✓'));
|
|
261
|
+
} catch (e) {
|
|
262
|
+
console.log(chalk.red(`✗ (${e.message})`));
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
237
266
|
|
|
238
267
|
// Initialize rules testing context for security rules tests
|
|
239
268
|
process.stdout.write(chalk.gray(' Initializing rules testing context... '));
|
|
@@ -253,23 +282,30 @@ class TestRunner {
|
|
|
253
282
|
}
|
|
254
283
|
|
|
255
284
|
/**
|
|
256
|
-
* Ensure meta/stats document
|
|
257
|
-
*
|
|
285
|
+
* Ensure meta/stats document has a baseline `users` counter (required for user
|
|
286
|
+
* count increments and the admin stats route).
|
|
287
|
+
*
|
|
288
|
+
* Uses a MERGE write that always runs — it does NOT early-return when the doc
|
|
289
|
+
* already exists. The reason: the preceding emulator wipe recursively deletes
|
|
290
|
+
* the `notifications` collection, which fires the notification on-write *delete*
|
|
291
|
+
* trigger for each doc. Those triggers merge `{ notifications: increment(-1) }`
|
|
292
|
+
* into meta/stats and can race ahead of this seed, re-creating the doc as
|
|
293
|
+
* `{ notifications: { total: -N } }` with NO `users` field. A plain
|
|
294
|
+
* "create-if-missing" seed would then skip (doc.exists === true) and leave
|
|
295
|
+
* `users` absent — exactly the bug that made the admin stats tests flaky.
|
|
296
|
+
*
|
|
297
|
+
* Merging `{ users: { total: 0 } }` is safe to run unconditionally: it seeds the
|
|
298
|
+
* baseline without clobbering whatever `notifications` value those triggers land,
|
|
299
|
+
* and the real user-count increments overwrite total: 0 as accounts are created.
|
|
258
300
|
*/
|
|
259
301
|
async ensureMetaStats() {
|
|
260
302
|
const admin = this.options.admin;
|
|
261
303
|
const statsRef = admin.firestore().doc('meta/stats');
|
|
262
304
|
|
|
263
|
-
const doc = await statsRef.get();
|
|
264
|
-
if (doc.exists) {
|
|
265
|
-
return; // Already exists, don't overwrite
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Create initial stats document
|
|
269
305
|
await statsRef.set({
|
|
270
306
|
users: { total: 0 },
|
|
271
307
|
brand: this.options.brand?.id,
|
|
272
|
-
});
|
|
308
|
+
}, { merge: true });
|
|
273
309
|
}
|
|
274
310
|
|
|
275
311
|
/**
|
|
@@ -284,6 +320,87 @@ class TestRunner {
|
|
|
284
320
|
}
|
|
285
321
|
}
|
|
286
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Load a single `test/_init.js` lifecycle hook from a test root.
|
|
325
|
+
*
|
|
326
|
+
* The module MUST export a function — `module.exports = (ctx) => ({ ... })` —
|
|
327
|
+
* called with `{ config, Manager }` and returning the hook object
|
|
328
|
+
* (`{ accounts, setup }`). This lets a project compute its accounts/fixtures
|
|
329
|
+
* from config at load time.
|
|
330
|
+
*
|
|
331
|
+
* Returns `{}` if the file doesn't exist, isn't a function, or fails to resolve.
|
|
332
|
+
*/
|
|
333
|
+
loadInit(testDir, label) {
|
|
334
|
+
const initPath = path.join(testDir, '_init.js');
|
|
335
|
+
|
|
336
|
+
if (!jetpack.exists(initPath)) {
|
|
337
|
+
return {};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const fn = require(initPath);
|
|
342
|
+
|
|
343
|
+
if (typeof fn !== 'function') {
|
|
344
|
+
console.log(chalk.red(` ✗ ${label} test/_init.js must export a function: module.exports = (ctx) => ({ ... })`));
|
|
345
|
+
return {};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const mod = fn({ config: this.config, Manager: this.config?.Manager });
|
|
349
|
+
return mod && typeof mod === 'object' ? mod : {};
|
|
350
|
+
} catch (e) {
|
|
351
|
+
console.log(chalk.red(` ✗ Failed to load ${label} test/_init.js: ${e.message}`));
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Load and merge the `test/_init.js` lifecycle hooks from BOTH test roots —
|
|
358
|
+
* BEM core (`<bem>/test/_init.js`) and the consumer project
|
|
359
|
+
* (`<projectDir>/test/_init.js`). Same contract for both, so framework and
|
|
360
|
+
* consumer authors write the identical file shape. Each exports a function
|
|
361
|
+
* (see loadInit) returning:
|
|
362
|
+
* - `accounts` — extra test accounts to create alongside the built-in ones,
|
|
363
|
+
* each `{ id, uid, email, properties }` (email may use the `{domain}`
|
|
364
|
+
* placeholder). One per lifecycle this project needs to exercise.
|
|
365
|
+
* - `async setup({ admin, config, accounts, Manager, assistant })` — seed
|
|
366
|
+
* fixtures (brands, etc.) AFTER the clean slate + account creation.
|
|
367
|
+
*
|
|
368
|
+
* There is no `cleanup` hook: the entire emulator Firestore is flushed before
|
|
369
|
+
* every run (deleteTestUsers → flushEmulatorFirestore) and each test cleans up
|
|
370
|
+
* after itself, so there is nothing project-level to tear down.
|
|
371
|
+
*
|
|
372
|
+
* Returns the merged extra `accounts` map (BEM core then consumer; consumer
|
|
373
|
+
* wins on key collision) and the ordered `setups` runners (BEM core first).
|
|
374
|
+
*/
|
|
375
|
+
loadInitHooks() {
|
|
376
|
+
const bemTestsDir = path.resolve(__dirname, '../../test');
|
|
377
|
+
const projectTestsDir = path.join(this.options.projectDir, 'test');
|
|
378
|
+
|
|
379
|
+
const hooks = [
|
|
380
|
+
this.loadInit(bemTestsDir, 'BEM core'),
|
|
381
|
+
this.loadInit(projectTestsDir, 'project'),
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
// Merge extra accounts from both roots. Accept either an array of account
|
|
385
|
+
// defs or a keyed object; normalize to a keyed object on `id`.
|
|
386
|
+
const accounts = {};
|
|
387
|
+
for (const h of hooks) {
|
|
388
|
+
const list = Array.isArray(h.accounts)
|
|
389
|
+
? h.accounts
|
|
390
|
+
: Object.values(h.accounts || {});
|
|
391
|
+
for (const account of list) {
|
|
392
|
+
if (account && account.id) {
|
|
393
|
+
accounts[account.id] = account;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
accounts,
|
|
400
|
+
setups: hooks.filter((h) => typeof h.setup === 'function').map((h) => h.setup),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
287
404
|
/**
|
|
288
405
|
* Discover test files in directory
|
|
289
406
|
*/
|
|
@@ -297,6 +414,11 @@ class TestRunner {
|
|
|
297
414
|
continue;
|
|
298
415
|
}
|
|
299
416
|
|
|
417
|
+
// Skip the _init.js lifecycle hook — it's run by setupAccounts(), not as a test.
|
|
418
|
+
if (item === '_init.js') {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
300
422
|
// Skip legacy 'functions' directory unless --legacy flag is set
|
|
301
423
|
if (item === 'functions' && !this.options.includeLegacy) {
|
|
302
424
|
continue;
|