bulltrackers-module 1.0.992 → 1.0.993

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.
@@ -24,6 +24,7 @@ const logsRoutes = require('./logs');
24
24
  const dashboardRoutes = require('./dashboard');
25
25
  const devRoutes = require('./dev');
26
26
  const accountRoutes = require('./account');
27
+ const tutorialRoutes = require('./tutorial');
27
28
 
28
29
  /**
29
30
  * Create and configure all API routes.
@@ -51,6 +52,7 @@ function createRoutes() {
51
52
  router.use('/logs', logsRoutes);
52
53
  router.use('/dev', devRoutes);
53
54
  router.use('/account', accountRoutes);
55
+ router.use('/tutorial', tutorialRoutes);
54
56
 
55
57
  // Legacy route compatibility: /user/:userId/sync
56
58
  router.post('/user/:userId/sync', async (req, res, next) => {
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @fileoverview Tutorial Progress Routes
3
+ * Tracks per-user completion of onboarding/tutorial tasks in Firestore.
4
+ */
5
+
6
+ const express = require('express');
7
+ const { z } = require('zod');
8
+ const { requireVerifiedUser } = require('../middleware/identity');
9
+
10
+ const router = express.Router();
11
+
12
+ const completeSchema = z.object({
13
+ taskId: z.string().min(1).max(200),
14
+ });
15
+
16
+ const restartSchema = z.object({
17
+ taskId: z.string().min(1).max(200),
18
+ /** Optional hint about restart strategy from frontend; backend still validates. */
19
+ restartStrategy: z.enum(['tutorialStateOnly', 'fullReset']).optional(),
20
+ });
21
+
22
+ function getTutorialProgressRef(db, cid) {
23
+ return db
24
+ .collection('SignedInUsers')
25
+ .doc(String(cid))
26
+ .collection('tutorial')
27
+ .doc('progress');
28
+ }
29
+
30
+ async function getTutorialConfig(req) {
31
+ const config = (req.config && req.config.tutorial) || {};
32
+ return {
33
+ nonRestartable: new Set(config.nonRestartableTaskIds || []),
34
+ fullReset: new Set(config.fullResetTaskIds || []),
35
+ };
36
+ }
37
+
38
+ router.get('/progress', requireVerifiedUser, async (req, res, next) => {
39
+ try {
40
+ const { db } = req.services;
41
+ const cid = req.targetUserId;
42
+
43
+ const doc = await getTutorialProgressRef(db, cid).get();
44
+ const data = doc.exists ? doc.data() : {};
45
+
46
+ res.json({
47
+ success: true,
48
+ data: {
49
+ completedTaskIds: Array.isArray(data.completedTaskIds)
50
+ ? data.completedTaskIds
51
+ : [],
52
+ explicitlyDismissedTaskIds: Array.isArray(
53
+ data.explicitlyDismissedTaskIds
54
+ )
55
+ ? data.explicitlyDismissedTaskIds
56
+ : [],
57
+ taskMetadata: data.taskMetadata || {},
58
+ },
59
+ });
60
+ } catch (error) {
61
+ next(error);
62
+ }
63
+ });
64
+
65
+ router.post('/progress/complete', requireVerifiedUser, async (req, res, next) => {
66
+ try {
67
+ const parsed = completeSchema.parse(req.body || {});
68
+ const { db } = req.services;
69
+ const cid = req.targetUserId;
70
+
71
+ const ref = getTutorialProgressRef(db, cid);
72
+ await db.runTransaction(async (tx) => {
73
+ const snap = await tx.get(ref);
74
+ const existing = snap.exists ? snap.data() || {} : {};
75
+
76
+ const completed = new Set(
77
+ Array.isArray(existing.completedTaskIds)
78
+ ? existing.completedTaskIds
79
+ : []
80
+ );
81
+ completed.add(parsed.taskId);
82
+
83
+ const meta = existing.taskMetadata || {};
84
+ const now = new Date();
85
+ const current = meta[parsed.taskId] || {};
86
+
87
+ tx.set(
88
+ ref,
89
+ {
90
+ completedTaskIds: Array.from(completed),
91
+ taskMetadata: {
92
+ ...meta,
93
+ [parsed.taskId]: {
94
+ ...current,
95
+ lastCompletedAt: now,
96
+ runCount: (current.runCount || 0) + 1,
97
+ },
98
+ },
99
+ },
100
+ { merge: true }
101
+ );
102
+ });
103
+
104
+ res.json({ success: true });
105
+ } catch (error) {
106
+ next(error);
107
+ }
108
+ });
109
+
110
+ router.post('/progress/restart', requireVerifiedUser, async (req, res, next) => {
111
+ try {
112
+ const parsed = restartSchema.parse(req.body || {});
113
+ const { db } = req.services;
114
+ const cid = req.targetUserId;
115
+ const { nonRestartable, fullReset } = await getTutorialConfig(req);
116
+
117
+ if (nonRestartable.has(parsed.taskId)) {
118
+ return res.status(400).json({
119
+ success: false,
120
+ error: 'NonRestartable',
121
+ message: 'This tutorial task cannot be restarted once completed.',
122
+ });
123
+ }
124
+
125
+ const shouldFullReset =
126
+ parsed.restartStrategy === 'fullReset' || fullReset.has(parsed.taskId);
127
+
128
+ if (shouldFullReset) {
129
+ const collectionPath = `SignedInUsers/${cid}`;
130
+ if (parsed.taskId === 'alerts_create_threshold') {
131
+ const alertsRef = db
132
+ .collection(collectionPath)
133
+ .doc(String(cid))
134
+ .collection('alert_subscriptions');
135
+ const snap = await alertsRef.limit(50).get();
136
+ const batch = db.batch();
137
+ snap.docs.forEach((doc) => {
138
+ const data = doc.data() || {};
139
+ if (data.source === 'tutorial') {
140
+ batch.delete(doc.ref);
141
+ }
142
+ });
143
+ await batch.commit();
144
+ }
145
+ }
146
+
147
+ const ref = getTutorialProgressRef(db, cid);
148
+ await db.runTransaction(async (tx) => {
149
+ const snap = await tx.get(ref);
150
+ const existing = snap.exists ? snap.data() || {} : {};
151
+
152
+ const completed = new Set(
153
+ Array.isArray(existing.completedTaskIds)
154
+ ? existing.completedTaskIds
155
+ : []
156
+ );
157
+ completed.delete(parsed.taskId);
158
+
159
+ const meta = existing.taskMetadata || {};
160
+ const now = new Date();
161
+ const current = meta[parsed.taskId] || {};
162
+
163
+ tx.set(
164
+ ref,
165
+ {
166
+ completedTaskIds: Array.from(completed),
167
+ taskMetadata: {
168
+ ...meta,
169
+ [parsed.taskId]: {
170
+ ...current,
171
+ lastRestartedAt: now,
172
+ restartedCount: (current.restartedCount || 0) + 1,
173
+ },
174
+ },
175
+ },
176
+ { merge: true }
177
+ );
178
+ });
179
+
180
+ res.json({ success: true, fullResetApplied: shouldFullReset });
181
+ } catch (error) {
182
+ next(error);
183
+ }
184
+ });
185
+
186
+ module.exports = router;
187
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.992",
3
+ "version": "1.0.993",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [