musora-content-services 2.164.6 → 2.165.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.
@@ -16,7 +16,9 @@
16
16
  "Bash(grep:*)",
17
17
  "Bash(gh api repos/railroadmedia/musora-platform-backend/commits/1f9878a5e681bf1e2233c0d59a618d477b118c49 --jq '.commit.message' 2>/dev/null || echo \"no access\")",
18
18
  "WebFetch(domain:github.com)",
19
- "Read(//app/**)"
19
+ "Read(//app/**)",
20
+ "Bash(git -C /app/musora-platform-backend log --oneline main..HEAD)",
21
+ "Bash(git -C /app/musora-platform-backend diff main..HEAD --name-only)"
20
22
  ]
21
23
  }
22
24
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
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.165.0](https://github.com/railroadmedia/musora-content-services/compare/v2.164.6...v2.165.0) (2026-06-18)
6
+
7
+
8
+ ### Features
9
+
10
+ * **BEHSTP-370:** add need_lifetime_upgrade flag on documents and grant30DaysAccessForLifetime method ([2dd8bed](https://github.com/railroadmedia/musora-content-services/commit/2dd8bedf71a543599a0c421d7eea99cae3bf156c))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * add method for progress types mapping ([#998](https://github.com/railroadmedia/musora-content-services/issues/998)) ([9830710](https://github.com/railroadmedia/musora-content-services/commit/9830710893e85f3680286fc5abdc23d1cbf47580))
16
+
5
17
  ### [2.164.6](https://github.com/railroadmedia/musora-content-services/compare/v2.164.5...v2.164.6) (2026-06-10)
6
18
 
7
19
  ### [2.164.5](https://github.com/railroadmedia/musora-content-services/compare/v2.164.4...v2.164.5) (2026-06-10)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.164.6",
3
+ "version": "2.165.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -73,6 +73,7 @@ export const DEFAULT_FIELDS = [
73
73
  'live_event_end_time',
74
74
  'enrollment_start_time',
75
75
  'enrollment_end_time',
76
+ 'membership_tier',
76
77
  ]
77
78
 
78
79
  // these are identical... why
@@ -328,6 +329,7 @@ export const progressTypesMapping = {
328
329
  'course-lesson',
329
330
  'routine',
330
331
  ],
332
+ method: [LEARNING_PATH_LESSON],
331
333
  course: ['course'],
332
334
  show: showsLessonTypes,
333
335
  'song tutorial': [...tutorialsLessonTypes, 'song-tutorial-lesson'],
@@ -695,9 +697,7 @@ export let contentTypeConfig = {
695
697
  }`,
696
698
  ],
697
699
  'new-and-scheduled': {
698
- fields: [
699
- 'show_in_new_feed',
700
- ],
700
+ fields: ['show_in_new_feed'],
701
701
  includeChildFields: true,
702
702
  },
703
703
  }
package/src/index.d.ts CHANGED
@@ -428,6 +428,7 @@ import {
428
428
  fetchMemberships,
429
429
  fetchRechargeTokens,
430
430
  getUpgradePrice,
431
+ grant30DaysAccessForLifetime,
431
432
  restorePurchases,
432
433
  upgradeSubscription
433
434
  } from './services/user/memberships.ts';
@@ -742,6 +743,7 @@ declare module 'musora-content-services' {
742
743
  getUserWeeklyStats,
743
744
  getWeekNumber,
744
745
  globalConfig,
746
+ grant30DaysAccessForLifetime,
745
747
  guidedCourses,
746
748
  hasAnyMethodV2IntroCompleted,
747
749
  initializeEnvVar,
package/src/index.js CHANGED
@@ -432,6 +432,7 @@ import {
432
432
  fetchMemberships,
433
433
  fetchRechargeTokens,
434
434
  getUpgradePrice,
435
+ grant30DaysAccessForLifetime,
435
436
  restorePurchases,
436
437
  upgradeSubscription
437
438
  } from './services/user/memberships.ts';
@@ -741,6 +742,7 @@ export {
741
742
  getUserWeeklyStats,
742
743
  getWeekNumber,
743
744
  globalConfig,
745
+ grant30DaysAccessForLifetime,
744
746
  guidedCourses,
745
747
  hasAnyMethodV2IntroCompleted,
746
748
  initializeEnvVar,
@@ -0,0 +1,48 @@
1
+ import { decorate, type FieldDecorator } from './base'
2
+ import { type UserPermissions } from '../../../services/permissions'
3
+ import { type AccessDecoratable } from './need-access'
4
+ import { MEMBERSHIP_PERMISSIONS } from '../../../constants/membership-permissions'
5
+
6
+ export const NEED_LIFETIME_UPGRADE_FIELD = 'need_lifetime_upgrade' as const
7
+
8
+ type LifetimeUpgradeDecoratable = AccessDecoratable & { need_access?: boolean; membership_tier?: string }
9
+
10
+ export type WithNeedLifetimeUpgrade<T extends AccessDecoratable> = T & {
11
+ need_lifetime_upgrade: boolean
12
+ children?: WithNeedLifetimeUpgrade<NonNullable<T['children']>[number]>[]
13
+ }
14
+
15
+ export function lifetimeUpgradeDecorator(
16
+ userPermissions: UserPermissions
17
+ ): FieldDecorator<LifetimeUpgradeDecoratable, typeof NEED_LIFETIME_UPGRADE_FIELD, boolean> {
18
+ const userPermSet = new Set(userPermissions?.permissions ?? [])
19
+ const hasLifetime = userPermSet.has(MEMBERSHIP_PERMISSIONS.lifetime)
20
+ const hasPlus = userPermSet.has(MEMBERSHIP_PERMISSIONS.plus)
21
+
22
+ return {
23
+ field: NEED_LIFETIME_UPGRADE_FIELD,
24
+ compute: (item) => {
25
+ if (userPermissions?.isAdmin || !hasLifetime || hasPlus) return false
26
+ if (!item.need_access) return false
27
+ return item.membership_tier === 'plus'
28
+ },
29
+ }
30
+ }
31
+
32
+ export function decorateLifetimeUpgrade<T extends LifetimeUpgradeDecoratable>(
33
+ items: T[],
34
+ userPermissions: UserPermissions
35
+ ): WithNeedLifetimeUpgrade<T>[]
36
+ export function decorateLifetimeUpgrade<T extends LifetimeUpgradeDecoratable>(
37
+ items: T,
38
+ userPermissions: UserPermissions
39
+ ): WithNeedLifetimeUpgrade<T>
40
+ export function decorateLifetimeUpgrade<T extends LifetimeUpgradeDecoratable>(
41
+ items: T | T[],
42
+ userPermissions: UserPermissions
43
+ ): WithNeedLifetimeUpgrade<T> | WithNeedLifetimeUpgrade<T>[] {
44
+ const { field, compute } = lifetimeUpgradeDecorator(userPermissions)
45
+ return decorate(items as T, field, compute) as
46
+ | WithNeedLifetimeUpgrade<T>
47
+ | WithNeedLifetimeUpgrade<T>[]
48
+ }
@@ -40,5 +40,6 @@ export interface Lesson {
40
40
  title: string
41
41
  type: LessonType
42
42
  need_access: boolean
43
+ need_lifetime_upgrade: boolean
43
44
  page_type: LessonPageType
44
45
  }
@@ -46,6 +46,7 @@ import { globalConfig } from './config.js'
46
46
 
47
47
  import { arrayToStringRepresentation, FilterBuilder } from '../filterBuilder.js'
48
48
  import { getPermissionsAdapter } from './permissions/index.ts'
49
+ import { lifetimeUpgradeDecorator, NEED_LIFETIME_UPGRADE_FIELD } from '../lib/sanity/decorators/need-lifetime-upgrade.ts'
49
50
  import {
50
51
  getAllCompleted,
51
52
  getAllCompletedByIds,
@@ -1639,6 +1640,7 @@ export async function fetchSanity(
1639
1640
  return null
1640
1641
  }
1641
1642
  results = processNeedAccess ? await needsAccessDecorator(results, userPermissions) : results
1643
+ results = processNeedAccess ? needsLifetimeUpgradeDecorator(results, userPermissions) : results
1642
1644
  results = processPageType ? pageTypeDecorator(results) : results
1643
1645
  return customPostProcess ? customPostProcess(results) : results
1644
1646
  } else {
@@ -1734,6 +1736,12 @@ function needsAccessDecorator(results, userPermissions) {
1734
1736
  })
1735
1737
  }
1736
1738
 
1739
+ function needsLifetimeUpgradeDecorator(results, userPermissions) {
1740
+ if (globalConfig.sanityConfig.useDummyRailContentMethods) return results
1741
+ const { compute } = lifetimeUpgradeDecorator(userPermissions)
1742
+ return contentResultsDecorator(results, NEED_LIFETIME_UPGRADE_FIELD, compute)
1743
+ }
1744
+
1737
1745
  function doesUserNeedAccessToContent(result, userPermissions) {
1738
1746
  // Legacy function - now delegates to adapter
1739
1747
  // Kept for backwards compatibility if used elsewhere
@@ -50,7 +50,9 @@ export interface UpgradeProduct {
50
50
  price: number
51
51
  monthly_price: number
52
52
  includes_trial: boolean
53
- tier: 'plus' | 'basic' | ''
53
+ tier: 'plus' | 'basic' | '' // deprecated in favour of membership_level
54
+ membership_level: 'plus' | 'basic' | ''
55
+ plan_type: 'family' | 'duo' | 'solo'
54
56
  }
55
57
 
56
58
  export interface UpgradeOption {
@@ -289,3 +291,11 @@ export async function fetchHasActivePlatformSubscription(): Promise<boolean> {
289
291
  const response = await httpClient.get<SubscriptionPlatform>(`${baseUrl}/v1/subscription-platform`)
290
292
  return response.has_active_platform_subscription
291
293
  }
294
+
295
+ /**
296
+ * @returns {Promise<void>}
297
+ */
298
+ export async function grant30DaysAccessForLifetime(): Promise<void> {
299
+ const httpClient = new HttpClient(globalConfig.baseUrl)
300
+ await httpClient.post<void>(`${baseUrl}/v1/grant-30-days-access-for-lifetime`, {})
301
+ }
File without changes
File without changes
@@ -0,0 +1,85 @@
1
+ import {
2
+ NEED_LIFETIME_UPGRADE_FIELD,
3
+ lifetimeUpgradeDecorator,
4
+ decorateLifetimeUpgrade,
5
+ } from '../../../../../src/lib/sanity/decorators/need-lifetime-upgrade'
6
+
7
+ const PLUS = 92
8
+ const LIFETIME = 108
9
+ const BASE = 91
10
+
11
+ const lifetimeBase = { permissions: [LIFETIME, BASE], isAdmin: false, isModerator: false, isABasicMember: true }
12
+ const lifetimeBasePlus = { permissions: [LIFETIME, BASE, PLUS], isAdmin: false, isModerator: false, isABasicMember: false }
13
+ const baseOnly = { permissions: [BASE], isAdmin: false, isModerator: false, isABasicMember: true }
14
+
15
+ const plusGatedLocked = { permission_id: [BASE, PLUS], need_access: true, membership_tier: 'plus' }
16
+ const plusGatedUnlocked = { permission_id: [BASE, PLUS], need_access: false, membership_tier: 'plus' }
17
+
18
+ describe('need-lifetime-upgrade decorator', () => {
19
+ describe('lifetimeUpgradeDecorator (factory)', () => {
20
+ test('field is need_lifetime_upgrade', () => {
21
+ const dec = lifetimeUpgradeDecorator(lifetimeBase)
22
+ expect(dec.field).toBe('need_lifetime_upgrade')
23
+ expect(NEED_LIFETIME_UPGRADE_FIELD).toBe('need_lifetime_upgrade')
24
+ })
25
+
26
+ test('true for lifetime member on Plus-gated content they cannot access', () => {
27
+ const dec = lifetimeUpgradeDecorator(lifetimeBase)
28
+ expect(dec.compute(plusGatedLocked)).toBe(true)
29
+ })
30
+
31
+ test('false when user already has Plus', () => {
32
+ const dec = lifetimeUpgradeDecorator(lifetimeBasePlus)
33
+ expect(dec.compute(plusGatedLocked)).toBe(false)
34
+ })
35
+
36
+ test('false when user does not have lifetime', () => {
37
+ const dec = lifetimeUpgradeDecorator(baseOnly)
38
+ expect(dec.compute(plusGatedLocked)).toBe(false)
39
+ })
40
+
41
+ test('false when user already has access to content (need_access false)', () => {
42
+ const dec = lifetimeUpgradeDecorator(lifetimeBase)
43
+ expect(dec.compute(plusGatedUnlocked)).toBe(false)
44
+ })
45
+ })
46
+
47
+ describe('decorateLifetimeUpgrade', () => {
48
+ test('decorates array of items', () => {
49
+ const items = [plusGatedLocked, plusGatedUnlocked]
50
+ const decorated = decorateLifetimeUpgrade(items, lifetimeBase)
51
+ expect(decorated.map((i) => i.need_lifetime_upgrade)).toEqual([true, false])
52
+ })
53
+
54
+ test('decorates a single item', () => {
55
+ const decorated = decorateLifetimeUpgrade(plusGatedLocked, lifetimeBase)
56
+ expect(decorated.need_lifetime_upgrade).toBe(true)
57
+ })
58
+
59
+ test('decorates children recursively', () => {
60
+ const tree = {
61
+ permission_id: [BASE, PLUS],
62
+ need_access: true,
63
+ membership_tier: 'plus',
64
+ children: [
65
+ {
66
+ permission_id: [BASE, PLUS],
67
+ need_access: true,
68
+ membership_tier: 'plus',
69
+ children: [{ permission_id: [BASE, PLUS], need_access: true, membership_tier: 'plus' }],
70
+ },
71
+ ],
72
+ }
73
+ const decorated = decorateLifetimeUpgrade(tree, lifetimeBase)
74
+ expect(decorated.need_lifetime_upgrade).toBe(true)
75
+ expect(decorated.children![0].need_lifetime_upgrade).toBe(true)
76
+ expect(decorated.children![0].children![0].need_lifetime_upgrade).toBe(true)
77
+ })
78
+
79
+ test('returns the same reference it was given', () => {
80
+ const items = [plusGatedLocked]
81
+ const decorated = decorateLifetimeUpgrade(items, lifetimeBase)
82
+ expect(decorated).toBe(items)
83
+ })
84
+ })
85
+ })
File without changes