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.
- package/.claude/settings.local.json +3 -1
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +3 -3
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/lib/sanity/decorators/need-lifetime-upgrade.ts +48 -0
- package/src/services/content/content.ts +1 -0
- package/src/services/sanity.js +8 -0
- package/src/services/user/memberships.ts +11 -1
- package/src/services/user/playerSettings.ts +0 -0
- package/src/services/user/types.d.ts +0 -0
- package/test/unit/lib/sanity/decorators/need-lifetime-upgrade.test.ts +85 -0
- package/test/unit/playerSettings.test.ts +0 -0
|
@@ -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
package/src/contentTypeConfig.js
CHANGED
|
@@ -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
|
+
}
|
package/src/services/sanity.js
CHANGED
|
@@ -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
|