musora-content-services 2.180.0 → 2.182.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.
@@ -1,50 +1,16 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
- "Bash(npx jest *)",
5
- "Bash(npx tsc *)",
6
- "Skill(counselors)",
7
- "Bash(counselors ls *)",
8
- "Bash(counselors groups *)",
9
- "Bash(counselors run *)",
4
+ "Bash(rg:*)",
5
+ "Bash(npm run lint:*)",
6
+ "Bash(ls:*)",
10
7
  "Bash(npm test *)",
11
- "Bash(gh pr *)",
12
- "Bash(gh api *)",
13
- "Bash(mkdir -p /tmp/pr-review-v2)",
14
- "Read(//tmp/pr-review-v2/**)",
15
- "Bash(cat /home/alesevero/railenvironment/applications/musora-content-services/AGENTS.md)",
16
- "Bash(echo \"no AGENTS.md\")",
17
- "Bash(echo \"exit=$?\")",
18
- "Bash(git checkout *)",
19
- "Skill(pr)",
20
- "Skill(create-decision)",
21
- "mcp__github__pull_request_read",
22
- "mcp__github__push_files",
23
- "Bash(git push *)",
24
- "mcp__github__create_pull_request",
25
- "mcp__github__search_pull_requests",
26
- "Bash(git commit *)",
27
- "Bash(git add *)",
28
- "mcp__github__list_pull_requests",
29
- "mcp__sanity__get_schema",
30
- "mcp__sanity__query_documents",
31
- "Bash(git rm *)",
32
- "Bash(git rebase *)",
33
- "Bash(git fetch *)",
34
- "Bash(git --no-pager log --oneline -1)",
35
- "Bash(git --no-pager log --oneline -5)",
36
- "Bash(git ls-tree *)",
37
- "Bash(git --no-pager log --oneline -2)",
38
- "Bash(git --no-pager diff --stat HEAD~1)",
39
- "Bash(git check-ignore *)",
40
- "Bash(python3 -)",
41
- "Bash(git --no-pager log --oneline -3)",
42
- "Bash(mkdir *)",
43
- "Bash(git --no-pager log --oneline origin/main..HEAD)"
44
- ]
8
+ "Bash(npx jest *)"
9
+ ],
10
+ "deny": []
45
11
  },
46
- "model": "sonnet",
47
12
  "enabledMcpjsonServers": [
13
+ "1password",
48
14
  "atlassian",
49
15
  "figma",
50
16
  "google-workspace",
@@ -58,14 +24,10 @@
58
24
  "chrome-devtools",
59
25
  "railway",
60
26
  "github",
61
- "asana"
62
- ],
63
- "disabledMcpjsonServers": [
27
+ "asana",
64
28
  "nightwatch",
65
- "1password",
66
29
  "mysql-staging",
67
30
  "mysql-local",
68
31
  "mobile"
69
- ],
70
- "effortLevel": "medium"
32
+ ]
71
33
  }
Binary file
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [2.182.0](https://github.com/railroadmedia/musora-content-services/compare/v2.181.0...v2.182.0) (2026-09-21)
6
+
7
+
8
+ ### Features
9
+
10
+ * **BEHLTP-368:** placement quiz endpoints ([#1051](https://github.com/railroadmedia/musora-content-services/issues/1051)) ([248b4c9](https://github.com/railroadmedia/musora-content-services/commit/248b4c9e48b056a651d44c129d5d0cb3ce80777d))
11
+ * **BEHLTP-395:** add resetActiveLearningPath() ([#1055](https://github.com/railroadmedia/musora-content-services/issues/1055)) ([02df2df](https://github.com/railroadmedia/musora-content-services/commit/02df2dffd59844ff0c11a43f357efa6ed346a07f))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * move learning-paths.ts to my-path/ module ([#1054](https://github.com/railroadmedia/musora-content-services/issues/1054)) ([49fb9a9](https://github.com/railroadmedia/musora-content-services/commit/49fb9a983512e1d657285ddd13555d5ae9cb8e7b))
17
+
18
+ ## [2.181.0](https://github.com/railroadmedia/musora-content-services/compare/v2.180.0...v2.181.0) (2026-09-17)
19
+
20
+
21
+ ### Features
22
+
23
+ * allow merged config ([#1018](https://github.com/railroadmedia/musora-content-services/issues/1018)) ([8d8a9aa](https://github.com/railroadmedia/musora-content-services/commit/8d8a9aaed048e83b7dfeb15a7c12fabd199721f1))
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * **BR-717:** expire streak cache at calendar day boundary ([1f4481c](https://github.com/railroadmedia/musora-content-services/commit/1f4481cc5a71afa990c39d5927099b093a475c83))
29
+ * **BR-964:** unpin deleted playlists at fetch time ([#1052](https://github.com/railroadmedia/musora-content-services/issues/1052)) ([0367143](https://github.com/railroadmedia/musora-content-services/commit/036714377469beccbceae93d138a9d2280516395))
30
+
5
31
  ## [2.180.0](https://github.com/railroadmedia/musora-content-services/compare/v2.179.0...v2.180.0) (2026-09-04)
6
32
 
7
33
 
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/jsdoc.json CHANGED
@@ -6,6 +6,7 @@
6
6
  "src/services/content",
7
7
  "src/services/gamification",
8
8
  "src/services/multi-user-accounts",
9
+ "src/services/my-path",
9
10
  "src/services/awards",
10
11
  "src/services/api",
11
12
  "src/services/progress-row",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.180.0",
3
+ "version": "2.182.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.d.ts CHANGED
@@ -20,7 +20,9 @@ import {
20
20
  getMCSVersion,
21
21
  globalConfig,
22
22
  initializeEnvVar,
23
- initializeService
23
+ initializeService,
24
+ setSessionUserData,
25
+ updateSessionConfig
24
26
  } from './services/config.js';
25
27
 
26
28
  import {
@@ -53,23 +55,6 @@ import {
53
55
  unEnrollUserInGuidedCourse
54
56
  } from './services/content-org/guided-courses.ts';
55
57
 
56
- import {
57
- completeLearningPathIntroVideo,
58
- completeMethodIntroVideo,
59
- fetchLearningPathLessons,
60
- fetchLearningPathProgressCheckLessons,
61
- getActivePath,
62
- getDailySession,
63
- getEnrichedLearningPath,
64
- getEnrichedLearningPaths,
65
- getLearningPathLessonsByIds,
66
- isNextLessonLocked,
67
- mapContentToParent,
68
- resetAllLearningPaths,
69
- startLearningPath,
70
- updateDailySession
71
- } from './services/content-org/learning-paths.ts';
72
-
73
58
  import {
74
59
  addItemToPlaylist,
75
60
  createPlaylist,
@@ -243,6 +228,29 @@ import {
243
228
  updateMultiUserAccount
244
229
  } from './services/multi-user-accounts/multi-user-accounts.ts';
245
230
 
231
+ import {
232
+ completeLearningPathIntroVideo,
233
+ completeMethodIntroVideo,
234
+ fetchLearningPathLessons,
235
+ fetchLearningPathProgressCheckLessons,
236
+ getActivePath,
237
+ getDailySession,
238
+ getEnrichedLearningPath,
239
+ getEnrichedLearningPaths,
240
+ getLearningPathLessonsByIds,
241
+ isNextLessonLocked,
242
+ mapContentToParent,
243
+ resetActiveLearningPath,
244
+ resetAllLearningPaths,
245
+ startLearningPath,
246
+ updateDailySession
247
+ } from './services/my-path/learning-paths.ts';
248
+
249
+ import {
250
+ fetchQuizAnswers,
251
+ storeQuizAnswers
252
+ } from './services/my-path/placement-quiz.ts';
253
+
246
254
  import {
247
255
  deleteAllNotifications,
248
256
  deleteNotification,
@@ -687,6 +695,7 @@ declare module 'musora-content-services' {
687
695
  fetchPosts,
688
696
  fetchPracticeGoals,
689
697
  fetchPublicAnnouncement,
698
+ fetchQuizAnswers,
690
699
  fetchRecent,
691
700
  fetchRecentActivitiesActiveTabs,
692
701
  fetchRecentUserActivities,
@@ -862,6 +871,7 @@ declare module 'musora-content-services' {
862
871
  requestEmailChange,
863
872
  rescindInvite,
864
873
  reset,
874
+ resetActiveLearningPath,
865
875
  resetAllAwards,
866
876
  resetAllLearningPaths,
867
877
  resetPassword,
@@ -877,6 +887,7 @@ declare module 'musora-content-services' {
877
887
  sendAccountSetupEmail,
878
888
  sendPasswordResetEmail,
879
889
  sendRevenueCatPurchaseMetadata,
890
+ setSessionUserData,
880
891
  setStudentViewForUser,
881
892
  setUserPinnedProgressRow,
882
893
  setUserSignature,
@@ -885,6 +896,7 @@ declare module 'musora-content-services' {
885
896
  startLiveEventPolling,
886
897
  startOnboarding,
887
898
  status,
899
+ storeQuizAnswers,
888
900
  toDayjs,
889
901
  toLocalDay,
890
902
  togglePlaylistPrivate,
@@ -917,6 +929,7 @@ declare module 'musora-content-services' {
917
929
  updatePracticeGoals,
918
930
  updatePracticeNotes,
919
931
  updateProfileVisibility,
932
+ updateSessionConfig,
920
933
  updateThread,
921
934
  updateUserPractice,
922
935
  upgradeSubscription,
package/src/index.js CHANGED
@@ -24,7 +24,9 @@ import {
24
24
  getMCSVersion,
25
25
  globalConfig,
26
26
  initializeEnvVar,
27
- initializeService
27
+ initializeService,
28
+ setSessionUserData,
29
+ updateSessionConfig
28
30
  } from './services/config.js';
29
31
 
30
32
  import {
@@ -57,23 +59,6 @@ import {
57
59
  unEnrollUserInGuidedCourse
58
60
  } from './services/content-org/guided-courses.ts';
59
61
 
60
- import {
61
- completeLearningPathIntroVideo,
62
- completeMethodIntroVideo,
63
- fetchLearningPathLessons,
64
- fetchLearningPathProgressCheckLessons,
65
- getActivePath,
66
- getDailySession,
67
- getEnrichedLearningPath,
68
- getEnrichedLearningPaths,
69
- getLearningPathLessonsByIds,
70
- isNextLessonLocked,
71
- mapContentToParent,
72
- resetAllLearningPaths,
73
- startLearningPath,
74
- updateDailySession
75
- } from './services/content-org/learning-paths.ts';
76
-
77
62
  import {
78
63
  addItemToPlaylist,
79
64
  createPlaylist,
@@ -247,6 +232,29 @@ import {
247
232
  updateMultiUserAccount
248
233
  } from './services/multi-user-accounts/multi-user-accounts.ts';
249
234
 
235
+ import {
236
+ completeLearningPathIntroVideo,
237
+ completeMethodIntroVideo,
238
+ fetchLearningPathLessons,
239
+ fetchLearningPathProgressCheckLessons,
240
+ getActivePath,
241
+ getDailySession,
242
+ getEnrichedLearningPath,
243
+ getEnrichedLearningPaths,
244
+ getLearningPathLessonsByIds,
245
+ isNextLessonLocked,
246
+ mapContentToParent,
247
+ resetActiveLearningPath,
248
+ resetAllLearningPaths,
249
+ startLearningPath,
250
+ updateDailySession
251
+ } from './services/my-path/learning-paths.ts';
252
+
253
+ import {
254
+ fetchQuizAnswers,
255
+ storeQuizAnswers
256
+ } from './services/my-path/placement-quiz.ts';
257
+
250
258
  import {
251
259
  deleteAllNotifications,
252
260
  deleteNotification,
@@ -686,6 +694,7 @@ export {
686
694
  fetchPosts,
687
695
  fetchPracticeGoals,
688
696
  fetchPublicAnnouncement,
697
+ fetchQuizAnswers,
689
698
  fetchRecent,
690
699
  fetchRecentActivitiesActiveTabs,
691
700
  fetchRecentUserActivities,
@@ -861,6 +870,7 @@ export {
861
870
  requestEmailChange,
862
871
  rescindInvite,
863
872
  reset,
873
+ resetActiveLearningPath,
864
874
  resetAllAwards,
865
875
  resetAllLearningPaths,
866
876
  resetPassword,
@@ -876,6 +886,7 @@ export {
876
886
  sendAccountSetupEmail,
877
887
  sendPasswordResetEmail,
878
888
  sendRevenueCatPurchaseMetadata,
889
+ setSessionUserData,
879
890
  setStudentViewForUser,
880
891
  setUserPinnedProgressRow,
881
892
  setUserSignature,
@@ -884,6 +895,7 @@ export {
884
895
  startLiveEventPolling,
885
896
  startOnboarding,
886
897
  status,
898
+ storeQuizAnswers,
887
899
  toDayjs,
888
900
  toLocalDay,
889
901
  togglePlaylistPrivate,
@@ -916,6 +928,7 @@ export {
916
928
  updatePracticeGoals,
917
929
  updatePracticeNotes,
918
930
  updateProfileVisibility,
931
+ updateSessionConfig,
919
932
  updateThread,
920
933
  updateUserPractice,
921
934
  upgradeSubscription,
@@ -23,9 +23,28 @@ export let globalConfig = {
23
23
  */
24
24
  const excludeFromGeneratedIndex = []
25
25
 
26
+ /**
27
+ * @param {*} target
28
+ * @param {*} source
29
+ * @returns {*}
30
+ */
31
+ function deepMerge(target, source) {
32
+ if (source === undefined) return target
33
+ if (typeof source !== 'object' || source === null || Array.isArray(source)) return source
34
+
35
+ const merged = { ...target }
36
+ for (const key of Object.keys(source)) {
37
+ merged[key] = deepMerge(target?.[key], source[key])
38
+ }
39
+ return merged
40
+ }
41
+
26
42
  /**
27
43
  * Initializes the service with the given configuration.
28
44
  * This function must be called before using any other functions in this library.
45
+ * Each call replaces the prior config wholesale - pass the full config every time.
46
+ * For a partial update to just the session (e.g. userId resolving after boot),
47
+ * use {@link updateSessionConfig} instead.
29
48
  * Automatically initializes award definitions with 24-hour cache in the background.
30
49
  *
31
50
  * @param {Config} config - Configuration object containing API settings.
@@ -107,6 +126,18 @@ export function initializeService(config) {
107
126
  }
108
127
  }
109
128
 
129
+ /**
130
+ * Deep-merges partial session data (e.g. userId) into the existing sessionConfig
131
+ * without touching any other part of globalConfig. Use this for reactive updates
132
+ * (e.g. a watcher that re-syncs userId once a user resolves) instead of calling
133
+ * initializeService again, since initializeService replaces sessionConfig wholesale.
134
+ *
135
+ * @param {*} sessionConfig
136
+ */
137
+ export function updateSessionConfig(sessionConfig) {
138
+ globalConfig.sessionConfig = deepMerge(globalConfig.sessionConfig, sessionConfig)
139
+ }
140
+
110
141
  export function initializeEnvVar(config) {
111
142
  globalConfig.appEnv = config.appEnv
112
143
  }
@@ -3,7 +3,7 @@ import { db } from './sync'
3
3
  import { COLLECTION_ID_SELF, COLLECTION_TYPE, STATE } from './sync/models/ContentProgress'
4
4
  import { trackUserPractice } from './userActivity'
5
5
  import { getNextLessonLessonParentTypes } from '../contentTypeConfig.js'
6
- import { getDailySession, onLearningPathCompletedActions } from './content-org/learning-paths.ts'
6
+ import { getDailySession, onLearningPathCompletedActions } from './my-path/learning-paths.ts'
7
7
  import { duplicateProgressToALaCarteOffline } from './offline/progress.ts'
8
8
 
9
9
  /**
@@ -0,0 +1,123 @@
1
+
2
+
3
+ type ContentId = number
4
+ type Branch = "essentials" | "classical" | "blues"
5
+
6
+ interface LearningPath {
7
+ id: ContentId
8
+ thumbnail?: string
9
+ title: string
10
+ difficulty?: number
11
+ branch: Branch
12
+ intro_video_id?: number // only if we have intro videos
13
+ published_on?: string
14
+ // ... any other content-related fields we need
15
+ }
16
+
17
+ interface Connection {
18
+ id: string // `${source}:${target}`, stable key
19
+ source: ContentId // prerequisite LP
20
+ target: ContentId // up_next LP
21
+ branch: Branch // which branch this connection belongs to
22
+ }
23
+
24
+ interface BranchIndex {
25
+ node_ids: ContentId[]
26
+ edge_ids: string[]
27
+ }
28
+
29
+ interface CanonicalMapData {
30
+ nodes: Record<ContentId, LearningPath>
31
+ edges: Connection[]
32
+ branches: Record<Branch, BranchIndex>
33
+ }
34
+
35
+ // example CanonicalMapData:
36
+ const exampleCurriculumMap: CanonicalMapData = {
37
+ "nodes": {
38
+ "1": { "id": 1, "title": "Essentials 1", "branch": "essentials" },
39
+ "2": { "id": 2, "title": "Essentials 2", "branch": "essentials" },
40
+ "3": { "id": 3, "title": "Essentials 3", "branch": "essentials" },
41
+ "4": { "id": 4, "title": "Classical 1", "branch": "classical" },
42
+ "5": { "id": 5, "title": "Classical 2", "branch": "classical" },
43
+ "6": { "id": 6, "title": "Classical 3", "branch": "classical" },
44
+ "7": { "id": 7, "title": "Blues 1", "branch": "blues" },
45
+ "8": { "id": 8, "title": "Blues 2", "branch": "blues" },
46
+ "9": { "id": 9, "title": "Blues 3", "branch": "blues" },
47
+ },
48
+ "edges": [
49
+ { "id": "1-2", "source": 1, "target": 2, "branch": "essentials" },
50
+ { "id": "2-3", "source": 2, "target": 3, "branch": "essentials" },
51
+ { "id": "3-4", "source": 3, "target": 4, "branch": "classical" },
52
+ { "id": "4-5", "source": 4, "target": 5, "branch": "classical" },
53
+ { "id": "5-6", "source": 5, "target": 6, "branch": "classical" },
54
+ { "id": "3-7", "source": 3, "target": 7, "branch": "blues" },
55
+ { "id": "7-8", "source": 7, "target": 8, "branch": "blues" },
56
+ { "id": "8-9", "source": 8, "target": 9, "branch": "blues" },
57
+ ],
58
+ "branches": {
59
+ "essentials": {
60
+ "node_ids": [1, 2, 3],
61
+ "edge_ids": ["1-2", "2-3"],
62
+ },
63
+ "classical": {
64
+ "node_ids": [4, 5, 6],
65
+ "edge_ids": ["3-4", "4-5", "5-6"],
66
+ },
67
+ "blues": {
68
+ "node_ids": [7, 8, 9],
69
+ "edge_ids": ["3-7", "7-8", "8-9"],
70
+ },
71
+ }
72
+ }
73
+
74
+
75
+ const exampleSongSelectionDTO =
76
+ [
77
+ {
78
+ learning_path_id: 8,
79
+ song_id: 101,
80
+ song_title: "Fur Elise",
81
+ song_artist: "Beethoven",
82
+ song_difficulty: 3,
83
+ song_thumbnail: "https://example.com/fur_elise_thumbnail.jpg",
84
+ song_preview: "https://example.com/fur_elise_preview.mp3"
85
+ },
86
+ // ... more song selections
87
+ ]
88
+
89
+ const myPathMap = [
90
+ "path-node-1",
91
+ "path-node-2",
92
+ // ... more path nodes
93
+ ]
94
+
95
+ const pathNode = {
96
+ "_id": "path-node-1",
97
+ "name": "Learning Path 1",
98
+ "pathStreamArray": [
99
+ {
100
+ "lpReferences": [
101
+ "LP1" // learning path 1
102
+ ],
103
+ "isInABTest": true,
104
+ "specificInstrument": "all"
105
+ },
106
+ {
107
+ "lpReferences": [
108
+ "LP1.1", // LP1 split into 2 smaller LPs
109
+ "LP1.2"
110
+ ],
111
+ "isInABTest": true,
112
+ "specificInstrument": "all"
113
+ },
114
+ {
115
+ "lpReferences": [
116
+ "LP1_pad-only" // a pad-only version of LP1
117
+ ],
118
+ "isInABTest": false,
119
+ "specificInstrument": "pad" // matches Placement Quiz instrument options
120
+ }
121
+ ]
122
+ }
123
+
@@ -18,7 +18,8 @@ import {
18
18
  contentStatusReset,
19
19
  getAllCompletedByIds,
20
20
  getIdsWhereLastAccessedFromMethod,
21
- getProgressState,
21
+ getProgressState,
22
+ resetStatus,
22
23
  } from '../contentProgress.js'
23
24
  import { COLLECTION_ID_SELF, COLLECTION_TYPE, CollectionParameter, STATE } from '../sync/models/ContentProgress'
24
25
  import { db, SyncWriteDTO } from '../sync'
@@ -94,7 +95,7 @@ interface CollectionObject {
94
95
  * @param brand
95
96
  * @param userDate - local datetime. must have date and time - format 2025-10-31T13:45:00
96
97
  */
97
- export async function getDailySession(brand: string, userDate: Date) {
98
+ export async function getDailySession(brand: string, userDate: Date): Promise<DailySessionResponse | '' | null> {
98
99
  const dateWithTimezone = formatLocalDateTime(userDate)
99
100
  const key = dailySessionKey(brand, dateWithTimezone)
100
101
 
@@ -102,13 +103,13 @@ export async function getDailySession(brand: string, userDate: Date) {
102
103
  return await dataPromiseGET(dailySessionPromises, key, async () => {
103
104
  const url = `${LEARNING_PATHS_PATH}/daily-session/get?brand=${brand}&userDate=${encodeURIComponent(dateWithTimezone)}`
104
105
 
105
- const response = await GET(url) as DailySessionResponse | ''
106
+ const response = await GET(url)
106
107
 
107
108
  if (!response) {
108
109
  return await updateDailySession(brand, userDate, false)
109
110
  }
110
- return response as DailySessionResponse
111
- })
111
+ return response
112
+ }) as DailySessionResponse | ''
112
113
  } catch (error) {
113
114
  console.error('Error fetching daily session:', (error as any).message)
114
115
  return null
@@ -125,7 +126,7 @@ export async function updateDailySession(
125
126
  brand: string,
126
127
  userDate: Date,
127
128
  keepFirstLearningPath: boolean = false,
128
- ) {
129
+ ): Promise<DailySessionResponse | null> {
129
130
  const dateWithTimezone = formatLocalDateTime(userDate)
130
131
  const key = dailySessionKey(brand, dateWithTimezone)
131
132
  const url: string = `${LEARNING_PATHS_PATH}/daily-session/create`
@@ -155,7 +156,7 @@ function formatLocalDateTime(date: Date): string {
155
156
  * Gets user's active learning path.
156
157
  * @param brand
157
158
  */
158
- export async function getActivePath(brand: string) {
159
+ export async function getActivePath(brand: string): Promise<ActiveLearningPathResponse | null> {
159
160
  const url: string = `${LEARNING_PATHS_PATH}/active-path/get?brand=${brand}`
160
161
 
161
162
  return (await dataPromiseGET(activePathPromises, activePathKey(brand), () =>
@@ -168,7 +169,7 @@ export async function getActivePath(brand: string) {
168
169
  * @param brand
169
170
  * @param learningPathId
170
171
  */
171
- export async function startLearningPath(brand: string, learningPathId: number) {
172
+ export async function startLearningPath(brand: string, learningPathId: number): Promise<ActiveLearningPathResponse | null> {
172
173
  const url: string = `${LEARNING_PATHS_PATH}/active-path/set`
173
174
  const body = { brand: brand, learning_path_id: learningPathId }
174
175
 
@@ -185,6 +186,19 @@ export async function startLearningPath(brand: string, learningPathId: number) {
185
186
  return response
186
187
  }
187
188
 
189
+ /*
190
+ * Resets the user's active learning path and updates the daily session.
191
+ * @param {string} brand
192
+ * @param {number} learningPathId - The ID of the active learning path to reset.
193
+ * @param {Date} userDate - The user's local date.
194
+ * @returns {Promise<DailySessionResponse | '' | null>} - The updated daily session response or null if an error occurs.
195
+ */
196
+ export async function resetActiveLearningPath(brand: string, learningPathId: number, userDate: Date): Promise<DailySessionResponse | '' | null> {
197
+ const collection: CollectionObject = { id: learningPathId, type: COLLECTION_TYPE.LEARNING_PATH }
198
+ await resetStatus(learningPathId, collection)
199
+ return await updateDailySession(brand, userDate)
200
+ }
201
+
188
202
  function dataPromiseGET<T>(
189
203
  cache: Map<string, Promise<T>>,
190
204
  key: string,
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @module MyPathPlacementQuiz
3
+ */
4
+ import { HttpClient } from '../../infrastructure/http/HttpClient'
5
+ import { globalConfig } from '../config.js'
6
+ import { OnboardingRecommendedContent } from '@/services/user/onboarding'
7
+
8
+
9
+ const baseUrl = `/api/my-path/v1/placement-quiz`
10
+
11
+ interface PlacementQuizAnswers {
12
+ skill_level: 'new' | 'beginner' | 'intermediate' | 'advanced' | 'expert'
13
+ genres: string[]
14
+ gear: string[]
15
+ }
16
+
17
+ interface PlacementQuizResponse {
18
+ user_id: string
19
+ brand: string
20
+ answers: PlacementQuizAnswers
21
+ }
22
+
23
+ interface StorePlacementQuizResponse {
24
+ placement_quiz: PlacementQuizResponse
25
+ recommended_content: OnboardingRecommendedContent
26
+ error?: any
27
+ }
28
+
29
+ /**
30
+ * Fetches the placement quiz answers from the API.
31
+ *
32
+ * @param brand
33
+ * @returns {Promise<PlacementQuizResponse>} - A promise that resolves to the placement quiz answers.
34
+ * @throws {HttpError} - If the HTTP request fails.
35
+ */
36
+ export async function fetchQuizAnswers(brand: string): Promise<PlacementQuizResponse> {
37
+ const httpClient = new HttpClient(globalConfig.baseUrl)
38
+ return httpClient.get<PlacementQuizResponse>(`${baseUrl}?brand=${brand}`)
39
+ }
40
+
41
+ /**
42
+ * Stores the placement quiz answers to the API.
43
+ *
44
+ * @param brand
45
+ * @param answers
46
+ * @returns {Promise<PlacementQuizResponse>} - A promise that resolves to the stored placement quiz answers.
47
+ * @throws {HttpError} - If the HTTP request fails.
48
+ */
49
+ export async function storeQuizAnswers(brand: string, answers: PlacementQuizAnswers): Promise<StorePlacementQuizResponse> {
50
+ const body = {
51
+ brand,
52
+ answers,
53
+ }
54
+
55
+ const httpClient = new HttpClient(globalConfig.baseUrl)
56
+ return httpClient.post<StorePlacementQuizResponse>(baseUrl, body)
57
+ }
@@ -5,7 +5,7 @@ import {
5
5
  CollectionParameter,
6
6
  } from '../sync/models/ContentProgress'
7
7
  import { getHierarchies, getHierarchy } from '../sanity.js'
8
- import { onLearningPathCompletedActions } from '../content-org/learning-paths'
8
+ import { onLearningPathCompletedActions } from '../my-path/learning-paths'
9
9
  import { duplicateProgressToALaCarteOffline } from '../offline/progress'
10
10
  import {
11
11
  bubbleAndTrickleProgressesSafely,
@@ -22,7 +22,7 @@ const CACHE_EXPIRY_MS = 5 * 60 * 1000
22
22
  /**
23
23
  * Retrieves user's pinned data by brand, from localStorage or BE call.
24
24
  * @param brand
25
- * @returns {Promise<any|*|{id, type}>}
25
+ * @returns {Promise<any|*|{id, type}>} - only id and type
26
26
  */
27
27
  async function getUserPinnedItem(brand) {
28
28
  const key = getUserPinProgressKey()
@@ -170,7 +170,12 @@ export async function getProgressRows({ brand = 'drumeo', limit = 8 } = {}, opti
170
170
 
171
171
  const [contentCardMap, playlistCards, methodCard] = await getCards(brand, limit, userPinnedItem)
172
172
 
173
- const pinnedCard = await popPinnedItem(userPinnedItem, contentCardMap, playlistCards, methodCard)
173
+ let pinnedCard = null
174
+ try {
175
+ pinnedCard = await popPinnedItem(userPinnedItem, contentCardMap, playlistCards, methodCard, brand) // todo: catch errors in here
176
+ } catch (error) {
177
+ console.error('Error popping pinned item:', error)
178
+ }
174
179
 
175
180
  let allResultsLength = playlistCards.length + contentCardMap.size
176
181
  if (methodCard) {
@@ -207,7 +212,7 @@ async function getCards(brand, limit, userPinnedItem) {
207
212
  * If userPinnedItem is not found, generate the pinned card from scratch.
208
213
  *
209
214
  **/
210
- async function popPinnedItem(userPinnedItem, contentCardMap, playlistCards, methodCard) {
215
+ async function popPinnedItem(userPinnedItem, contentCardMap, playlistCards, methodCard, brand) {
211
216
  if (!userPinnedItem) return null
212
217
  const pinnedId = parseInt(userPinnedItem.id)
213
218
  const progressType = userPinnedItem.progressType ?? userPinnedItem.type
@@ -236,13 +241,24 @@ async function popPinnedItem(userPinnedItem, contentCardMap, playlistCards, meth
236
241
  if (pinnedPlaylist) {
237
242
  item = pinnedPlaylist
238
243
  } else {
239
- const playlist = await fetchPlaylist(pinnedId)
240
- item = processPlaylistItem({
241
- id: pinnedId,
242
- playlist: playlist,
243
- type: 'playlist',
244
- progressTimestamp: new Date().getTime(),
245
- })
244
+ let playlist
245
+ try {
246
+ playlist = await fetchPlaylist(pinnedId)
247
+ } catch (error) {
248
+ if (error?.status !== 404) {
249
+ throw error
250
+ }
251
+ unpinProgressRow(brand)
252
+ }
253
+
254
+ if (playlist) {
255
+ item = processPlaylistItem({
256
+ id: pinnedId,
257
+ playlist: playlist,
258
+ type: 'playlist',
259
+ progressTimestamp: new Date().getTime(),
260
+ })
261
+ }
246
262
  }
247
263
  } else if (progressType === 'method') {
248
264
  // simply get method card and return
@@ -2,7 +2,7 @@
2
2
  * @module ProgressRow
3
3
  */
4
4
 
5
- import { getActivePath, fetchLearningPathLessons } from '../../content-org/learning-paths'
5
+ import { getActivePath, fetchLearningPathLessons } from '../../my-path/learning-paths.ts'
6
6
  import { fetchMethodV2IntroVideo } from '../../sanity'
7
7
  import { getProgressState } from '../../contentProgress'
8
8
  import { COLLECTION_TYPE, STATE } from '../../sync/models/ContentProgress'
@@ -22,13 +22,17 @@ export interface PracticeData {
22
22
  class StreakCalculator {
23
23
  private cache: StreakData | null = null
24
24
  async getStreakData(): Promise<StreakData> {
25
- if (this.cache) {
25
+ if (this.cache && this.isCacheFromToday()) {
26
26
  return this.cache
27
27
  }
28
28
 
29
29
  return await this.recalculate()
30
30
  }
31
31
 
32
+ private isCacheFromToday(): boolean {
33
+ return dayjs(this.cache.calculatedAt).format('YYYY-MM-DD') === dayjs().format('YYYY-MM-DD')
34
+ }
35
+
32
36
  async recalculate(): Promise<StreakData> {
33
37
  const allPractices = await this.fetchAllPractices()
34
38
 
@@ -13,7 +13,7 @@ import dayjs from 'dayjs'
13
13
  import { addContextToContent } from './contentAggregator.js'
14
14
  import { db, Q } from './sync'
15
15
  import { streakCalculator } from './user/streakCalculator'
16
- import { mapContentsThatWereLastProgressedFromMethod } from "./content-org/learning-paths.ts";
16
+ import { mapContentsThatWereLastProgressedFromMethod } from "./my-path/learning-paths.ts";
17
17
 
18
18
  const DAYS = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
19
19
 
@@ -1 +1 @@
1
- export const MCS_VERSION = '2.180.0'
1
+ export const MCS_VERSION = '2.182.0'