backend-manager 5.0.163 → 5.0.165
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,15 @@ 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.165] - 2026-03-20
|
|
18
|
+
### Changed
|
|
19
|
+
- Serve command now reads hosting port from `firebase.json` emulator config before falling back to default 5000
|
|
20
|
+
- Notification test fixtures migrated from flat `createdAt`/`updatedAt` to nested `metadata.created`/`metadata.updated` objects matching standard BEM metadata format
|
|
21
|
+
|
|
22
|
+
# [5.0.164] - 2026-03-18
|
|
23
|
+
### Added
|
|
24
|
+
- Default field backfill in campaign seed setup — missing fields are restored from seed defaults without overwriting user edits
|
|
25
|
+
|
|
17
26
|
# [5.0.163] - 2026-03-18
|
|
18
27
|
### Changed
|
|
19
28
|
- Refactored campaign POST/PUT routes to generic field passthrough — schema-validated fields flow through automatically via shared `buildCampaignDoc()` utility, no manual field assignments needed
|
package/TODO-2.md
CHANGED
|
@@ -13,6 +13,31 @@ payments/upgrade
|
|
|
13
13
|
TEST NEWSLETTER
|
|
14
14
|
POST /admin/cron { id: 'daily/marketing-newsletter-generate' }
|
|
15
15
|
|
|
16
|
+
|
|
17
|
+
SIGNUP HANDLER
|
|
18
|
+
Here are the instructions for BEM:
|
|
19
|
+
|
|
20
|
+
Update updateReferral() in sign-up.js to resolve all legacy affiliate code formats:
|
|
21
|
+
|
|
22
|
+
The affiliateCode value coming from the client could be in 3 formats:
|
|
23
|
+
|
|
24
|
+
Format Example How to resolve
|
|
25
|
+
Affiliate code (7-14 alphanumeric) rmUKlC4z1 Query users where affiliate.code == value (current behavior)
|
|
26
|
+
UID (28 chars) 6sNjQFxTsObA73D8lkF01gcWdP92 Direct doc lookup: users/{value}
|
|
27
|
+
Base64 email (e.g. cG9ldH...Lm_2) cG9ldHJ5aW5hY3Rpb24yMDE4QGdtYWlsLmNvbQ_2 Strip _\d+ suffix, base64-decode, query users where auth.email == decoded
|
|
28
|
+
Any other format → ignore (resolve with no referrer).
|
|
29
|
+
|
|
30
|
+
The resolution logic should go at the top of updateReferral(), before the current where('affiliate.code', '==', affiliateCode) query. Try each format in order:
|
|
31
|
+
|
|
32
|
+
If affiliateCode matches /^[0-9a-zA-Z_-]{7,14}$/ → query by affiliate.code (current path)
|
|
33
|
+
Else if affiliateCode.length === 28 → direct doc get users/{affiliateCode}, use that as the referrer
|
|
34
|
+
Else try base64 decode: strip trailing _\d+, decode, if result contains @ → query by auth.email
|
|
35
|
+
Else → log and return (unrecognized format)
|
|
36
|
+
Once the referrer doc is found (by any method), the rest stays the same: push to affiliate.referrals.
|
|
37
|
+
|
|
38
|
+
* if the usage was used and the user is actuall authenticated (uid, not just an admin or unauthed user),
|
|
39
|
+
* set the user's context (ip, location, etc)
|
|
40
|
+
|
|
16
41
|
Payment attribution
|
|
17
42
|
* can you ensure the the user's attribution (utm etc) is assocaited with the purchase
|
|
18
43
|
* mostly i am referring to the payent events sent to GA4, tiktok, meta
|
package/package.json
CHANGED
|
@@ -8,8 +8,9 @@ const WatchCommand = require('./watch');
|
|
|
8
8
|
class ServeCommand extends BaseCommand {
|
|
9
9
|
async execute() {
|
|
10
10
|
const self = this.main;
|
|
11
|
-
const port = self.argv.port || self.argv?._?.[1] || '5000';
|
|
12
11
|
const projectDir = self.firebaseProjectPath;
|
|
12
|
+
const firebaseConfig = JSON.parse(fs.readFileSync(path.join(projectDir, 'firebase.json'), 'utf8'));
|
|
13
|
+
const port = self.argv.port || self.argv?._?.[1] || firebaseConfig?.emulators?.hosting?.port || '5000';
|
|
13
14
|
|
|
14
15
|
// Check for port conflicts before starting server
|
|
15
16
|
const canProceed = await this.checkAndKillBlockingProcesses({ serving: parseInt(port, 10) });
|
|
@@ -25,7 +25,7 @@ class MarketingCampaignsSeededTest extends BaseTest {
|
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// Check enforced fields
|
|
28
|
+
// Check enforced fields + missing defaults
|
|
29
29
|
const data = doc.data();
|
|
30
30
|
|
|
31
31
|
for (const [path, expected] of Object.entries(seed.enforced)) {
|
|
@@ -35,6 +35,11 @@ class MarketingCampaignsSeededTest extends BaseTest {
|
|
|
35
35
|
return false;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
// Check for missing fields that should exist from seed
|
|
40
|
+
if (hasMissingFields(data, seed.doc)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
return true;
|
|
@@ -62,10 +67,14 @@ class MarketingCampaignsSeededTest extends BaseTest {
|
|
|
62
67
|
continue;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
// Doc exists →
|
|
70
|
+
// Doc exists → fill missing defaults + enforce required fields
|
|
66
71
|
const data = doc.data();
|
|
67
72
|
const updates = {};
|
|
68
73
|
|
|
74
|
+
// Fill missing fields from seed defaults (never overwrite existing values)
|
|
75
|
+
fillMissing(data, seed.doc, updates, '');
|
|
76
|
+
|
|
77
|
+
// Enforce required fields (always overwrite to match seed)
|
|
69
78
|
for (const [path, expected] of Object.entries(seed.enforced)) {
|
|
70
79
|
const actual = _.get(data, path);
|
|
71
80
|
|
|
@@ -106,4 +115,58 @@ class MarketingCampaignsSeededTest extends BaseTest {
|
|
|
106
115
|
}
|
|
107
116
|
}
|
|
108
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Check if the live doc is missing any fields defined in the seed.
|
|
120
|
+
*/
|
|
121
|
+
function hasMissingFields(live, seed, prefix) {
|
|
122
|
+
for (const [key, seedValue] of Object.entries(seed)) {
|
|
123
|
+
if (key === 'metadata') {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
128
|
+
const liveValue = _.get(live, path);
|
|
129
|
+
|
|
130
|
+
if (liveValue === undefined) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (_.isPlainObject(seedValue) && _.isPlainObject(liveValue)) {
|
|
135
|
+
if (hasMissingFields(live, seedValue, path)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Recursively fill missing fields from seed into updates.
|
|
146
|
+
* Only sets fields that don't exist in the live doc — never overwrites.
|
|
147
|
+
* Skips metadata (managed separately).
|
|
148
|
+
*/
|
|
149
|
+
function fillMissing(live, seed, updates, prefix) {
|
|
150
|
+
for (const [key, seedValue] of Object.entries(seed)) {
|
|
151
|
+
if (key === 'metadata') {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
156
|
+
const liveValue = _.get(live, path);
|
|
157
|
+
|
|
158
|
+
// If live doc is missing this field entirely, set it from seed
|
|
159
|
+
if (liveValue === undefined) {
|
|
160
|
+
_.set(updates, path, seedValue);
|
|
161
|
+
console.log(chalk.blue(` + ${path}: ${chalk.dim('(missing)')} → ${chalk.bold(JSON.stringify(seedValue).slice(0, 80))}`));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// If both are plain objects, recurse to check nested fields
|
|
166
|
+
if (_.isPlainObject(seedValue) && _.isPlainObject(liveValue)) {
|
|
167
|
+
fillMissing(live, seedValue, updates, path);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
109
172
|
module.exports = MarketingCampaignsSeededTest;
|
|
@@ -31,7 +31,7 @@ module.exports = {
|
|
|
31
31
|
db.doc(`notifications/${token}`).set({
|
|
32
32
|
token: token,
|
|
33
33
|
owner: 'anonymous',
|
|
34
|
-
|
|
34
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
35
35
|
})
|
|
36
36
|
);
|
|
37
37
|
},
|
|
@@ -52,7 +52,7 @@ module.exports = {
|
|
|
52
52
|
db.doc(`notifications/${token}`).set({
|
|
53
53
|
token: token,
|
|
54
54
|
owner: uid,
|
|
55
|
-
|
|
55
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
56
56
|
})
|
|
57
57
|
);
|
|
58
58
|
},
|
|
@@ -74,7 +74,7 @@ module.exports = {
|
|
|
74
74
|
adminDb.doc(`notifications/${token}`).set({
|
|
75
75
|
token: token,
|
|
76
76
|
owner: uid,
|
|
77
|
-
|
|
77
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
78
78
|
})
|
|
79
79
|
);
|
|
80
80
|
|
|
@@ -101,7 +101,7 @@ module.exports = {
|
|
|
101
101
|
adminDb.doc(`notifications/${token}`).set({
|
|
102
102
|
token: token,
|
|
103
103
|
owner: otherUid,
|
|
104
|
-
|
|
104
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
105
105
|
})
|
|
106
106
|
);
|
|
107
107
|
|
|
@@ -144,14 +144,14 @@ module.exports = {
|
|
|
144
144
|
adminDb.doc(`notifications/${token}`).set({
|
|
145
145
|
token: token,
|
|
146
146
|
owner: otherUid,
|
|
147
|
-
|
|
147
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
148
148
|
})
|
|
149
149
|
);
|
|
150
150
|
|
|
151
151
|
// Should succeed - user can update if token matches
|
|
152
152
|
await rules.expectSuccess(
|
|
153
153
|
db.doc(`notifications/${token}`).update({
|
|
154
|
-
|
|
154
|
+
'metadata.updated': { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) },
|
|
155
155
|
})
|
|
156
156
|
);
|
|
157
157
|
},
|
|
@@ -173,7 +173,7 @@ module.exports = {
|
|
|
173
173
|
adminDb.doc(`notifications/${token}`).set({
|
|
174
174
|
token: token,
|
|
175
175
|
owner: uid,
|
|
176
|
-
|
|
176
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
177
177
|
})
|
|
178
178
|
);
|
|
179
179
|
|
|
@@ -203,7 +203,7 @@ module.exports = {
|
|
|
203
203
|
adminDb.doc(`notifications/${realToken}`).set({
|
|
204
204
|
token: realToken,
|
|
205
205
|
owner: otherUid,
|
|
206
|
-
|
|
206
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
207
207
|
})
|
|
208
208
|
);
|
|
209
209
|
|
|
@@ -237,7 +237,7 @@ module.exports = {
|
|
|
237
237
|
adminDb.doc(`notifications/${token}`).set({
|
|
238
238
|
token: token,
|
|
239
239
|
owner: uid,
|
|
240
|
-
|
|
240
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
241
241
|
})
|
|
242
242
|
);
|
|
243
243
|
|
|
@@ -269,7 +269,7 @@ module.exports = {
|
|
|
269
269
|
adminDb.doc(`notifications/${token}`).set({
|
|
270
270
|
token: token,
|
|
271
271
|
owner: accounts.basic.uid,
|
|
272
|
-
|
|
272
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
273
273
|
})
|
|
274
274
|
);
|
|
275
275
|
},
|
|
@@ -290,7 +290,7 @@ module.exports = {
|
|
|
290
290
|
adminDb.doc(`notifications/${token}`).set({
|
|
291
291
|
token: token,
|
|
292
292
|
owner: basicUid,
|
|
293
|
-
|
|
293
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
294
294
|
})
|
|
295
295
|
);
|
|
296
296
|
|
|
@@ -315,14 +315,14 @@ module.exports = {
|
|
|
315
315
|
adminDb.doc(`notifications/${token}`).set({
|
|
316
316
|
token: token,
|
|
317
317
|
owner: accounts.basic.uid,
|
|
318
|
-
|
|
318
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
319
319
|
})
|
|
320
320
|
);
|
|
321
321
|
|
|
322
322
|
// Should succeed - admin can update any doc
|
|
323
323
|
await rules.expectSuccess(
|
|
324
324
|
adminDb.doc(`notifications/${token}`).update({
|
|
325
|
-
|
|
325
|
+
'metadata.updated': { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) },
|
|
326
326
|
adminModified: true,
|
|
327
327
|
})
|
|
328
328
|
);
|
|
@@ -343,7 +343,7 @@ module.exports = {
|
|
|
343
343
|
adminDb.doc(`notifications/${token}`).set({
|
|
344
344
|
token: token,
|
|
345
345
|
owner: 'someone',
|
|
346
|
-
|
|
346
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
347
347
|
})
|
|
348
348
|
);
|
|
349
349
|
|
|
@@ -370,7 +370,7 @@ module.exports = {
|
|
|
370
370
|
adminDb.doc(`notifications/${token}`).set({
|
|
371
371
|
token: token,
|
|
372
372
|
owner: uid,
|
|
373
|
-
|
|
373
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
374
374
|
})
|
|
375
375
|
);
|
|
376
376
|
|
|
@@ -396,7 +396,7 @@ module.exports = {
|
|
|
396
396
|
adminDb.doc(`notifications/${token}`).set({
|
|
397
397
|
token: token,
|
|
398
398
|
owner: 'someone',
|
|
399
|
-
|
|
399
|
+
metadata: { created: { timestamp: new Date().toISOString(), timestampUNIX: Math.floor(Date.now() / 1000) } },
|
|
400
400
|
})
|
|
401
401
|
);
|
|
402
402
|
|