@faststore/core 4.6.0-dev.1 → 4.6.0-dev.3

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.
Files changed (29) hide show
  1. package/.turbo/turbo-generate.log +4 -4
  2. package/.turbo/turbo-test.log +104 -98
  3. package/@generated/cached-operations.json +1 -1
  4. package/@generated/gql.ts +9 -9
  5. package/@generated/graphql.ts +18 -12
  6. package/@generated/persisted-documents.json +2 -2
  7. package/@generated/schema.graphql +6 -0
  8. package/CHANGELOG.md +13 -0
  9. package/package.json +6 -6
  10. package/src/components/sections/RecommendationShelf/README.md +4 -1
  11. package/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +11 -92
  12. package/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +3 -18
  13. package/src/sdk/cart/index.ts +15 -1
  14. package/src/sdk/cart/waitForSessionValidated.ts +43 -0
  15. package/src/sdk/recommendations/index.ts +17 -0
  16. package/src/sdk/recommendations/types.ts +17 -0
  17. package/src/sdk/recommendations/useRecommendationShelf.ts +141 -0
  18. package/src/{components/sections/RecommendationShelf → sdk/recommendations}/vrn.ts +1 -1
  19. package/src/sdk/session/index.ts +14 -0
  20. package/src/sdk/session/syncSalesChannelFromOrderForm.ts +68 -0
  21. package/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +6 -12
  22. package/test/sdk/cart/waitForSessionValidated.test.ts +96 -0
  23. package/test/sdk/recommendations/useRecommendationShelf.browser.test.tsx +199 -0
  24. package/test/{components/RecommendationShelf → sdk/recommendations}/useRecommendationUserId.test.ts +1 -1
  25. package/test/{components/RecommendationShelf → sdk/recommendations}/useRecommendations.test.ts +1 -1
  26. package/test/{components/RecommendationShelf → sdk/recommendations}/vrn.test.ts +1 -4
  27. package/test/sdk/session/syncSalesChannelFromOrderForm.test.ts +125 -0
  28. /package/src/{components/sections/RecommendationShelf → sdk/recommendations}/useRecommendationUserId.ts +0 -0
  29. /package/src/{components/sections/RecommendationShelf → sdk/recommendations}/useRecommendations.ts +0 -0
@@ -0,0 +1,141 @@
1
+ import { usePDP } from '@faststore/core'
2
+ import { useMemo } from 'react'
3
+
4
+ import { useCart } from 'src/sdk/cart'
5
+
6
+ import type { ItemContext } from './types'
7
+ import {
8
+ type RecommendationInput,
9
+ type RecommendationResponse,
10
+ useRecommendations,
11
+ } from './useRecommendations'
12
+ import { useRecommendationUserId } from './useRecommendationUserId'
13
+ import { getTypeFromVrn } from './vrn'
14
+
15
+ export function getRecommendationArguments(
16
+ campaignVrn: string,
17
+ context: { userId?: string | null; contextProducts: string[] }
18
+ ): RecommendationInput | null {
19
+ const { userId, contextProducts } = context
20
+ const type = getTypeFromVrn(campaignVrn)
21
+
22
+ // `type` is null for malformed/unknown VRNs; bail out so we never fetch (and
23
+ // never throw) on an invalid campaign coming from the CMS.
24
+ if (!type || !userId) return null
25
+
26
+ switch (type) {
27
+ case 'NEXT_INTERACTION':
28
+ case 'VISUAL_SIMILARITY':
29
+ case 'CROSS_SELL':
30
+ case 'SIMILAR_ITEMS':
31
+ // These campaigns need product context. Without any (e.g. `CART` context
32
+ // on an empty cart, or `PDP` context outside a product page), skip the
33
+ // fetch instead of requesting recommendations we can't anchor.
34
+ if (contextProducts.length === 0) {
35
+ return null
36
+ }
37
+ return {
38
+ userId,
39
+ campaignVrn,
40
+ products: contextProducts,
41
+ }
42
+ default:
43
+ return {
44
+ userId,
45
+ campaignVrn,
46
+ products: [],
47
+ }
48
+ }
49
+ }
50
+
51
+ export type UseRecommendationShelfArgs = {
52
+ readonly campaignVrn: string
53
+ /**
54
+ * Opt-in for VTEX Recommendations. When `false` (default) the campaign is
55
+ * never requested. The anonymous user id is still resolved, but it goes
56
+ * unused and never leaves the browser.
57
+ */
58
+ readonly enableRecommendations?: boolean
59
+ /**
60
+ * Where to read the products used as context for the recommendation request.
61
+ * @default 'PDP'
62
+ */
63
+ readonly itemsContext?: ItemContext
64
+ }
65
+
66
+ export type UseRecommendationShelfResult = {
67
+ readonly items: RecommendationResponse['products']
68
+ readonly isLoading: boolean
69
+ readonly error: unknown
70
+ readonly campaign?: RecommendationResponse['campaign']
71
+ readonly correlationId?: string
72
+ }
73
+
74
+ const EMPTY_ITEMS: RecommendationResponse['products'] = []
75
+
76
+ /**
77
+ * Headless data layer behind every recommendation shelf: resolves the campaign
78
+ * type, the anonymous user id and the product context, then fetches the
79
+ * campaign. It renders nothing — presentation lives in the shelf components
80
+ * that consume it, so a new surface can reuse the campaign rules instead of
81
+ * reimplementing them.
82
+ */
83
+ export function useRecommendationShelf({
84
+ campaignVrn,
85
+ enableRecommendations = false,
86
+ itemsContext = 'PDP',
87
+ }: UseRecommendationShelfArgs): UseRecommendationShelfResult {
88
+ const userId = useRecommendationUserId(campaignVrn)
89
+
90
+ const { data: productDetailPage } = usePDP()
91
+ const { items: cartItems } = useCart()
92
+
93
+ // Resolve the products used as context for the request from the configured
94
+ // source: the current PDP product, or the (deduplicated) cart items.
95
+ const contextProducts = useMemo(() => {
96
+ if (itemsContext === 'CART') {
97
+ return Array.from(
98
+ new Set(
99
+ cartItems
100
+ .map((item) => item.itemOffered.isVariantOf.productGroupID)
101
+ .filter(Boolean)
102
+ )
103
+ )
104
+ }
105
+
106
+ const pdpProduct = productDetailPage?.product?.isVariantOf?.productGroupID
107
+
108
+ return pdpProduct ? [pdpProduct] : []
109
+ }, [itemsContext, cartItems, productDetailPage])
110
+
111
+ // Gate the whole feature behind the CMS opt-in: when Recommendations is
112
+ // disabled the shelf never requests recommendations (nor renders).
113
+ const recommendationArgs = enableRecommendations
114
+ ? getRecommendationArguments(campaignVrn, {
115
+ userId,
116
+ contextProducts,
117
+ })
118
+ : null
119
+
120
+ const { data, isLoading, error } = useRecommendations(recommendationArgs)
121
+
122
+ if (error) {
123
+ // Don't log `recommendationArgs`: it carries the userId. Log only
124
+ // non-identifying context.
125
+ console.error(
126
+ 'Error fetching recommendations',
127
+ (error as { cause?: unknown })?.cause,
128
+ (error as { message?: string })?.message,
129
+ 'for campaign',
130
+ campaignVrn
131
+ )
132
+ }
133
+
134
+ return {
135
+ items: error ? EMPTY_ITEMS : (data?.products ?? EMPTY_ITEMS),
136
+ isLoading,
137
+ error,
138
+ campaign: data?.campaign,
139
+ correlationId: data?.correlationId,
140
+ }
141
+ }
@@ -1,4 +1,4 @@
1
- import type { RecommendationType } from './RecommendationShelf.types'
1
+ import type { RecommendationType } from './types'
2
2
 
3
3
  // Single source of truth for the campaign taxonomy: maps each Intelligent Search
4
4
  // campaign VRN type to its `RecommendationType`. The supported VRN types, the
@@ -194,6 +194,9 @@ export const validateSession = async (session: Session) => {
194
194
  const [validationStore, onValidate, hasValidatedStore] =
195
195
  createValidationStore(validateSession)
196
196
 
197
+ /** True after at least one validateSession cycle finished (success or failure). */
198
+ export const hasValidatedSessionStore = hasValidatedStore
199
+
197
200
  const urlAwareInitialSession = getInitialSession()
198
201
  const defaultStore = createSessionStore(
199
202
  urlAwareInitialSession,
@@ -214,6 +217,17 @@ export const sessionStore = {
214
217
  // Trigger cart revalidation when session changes
215
218
  cartStore.set(cartStore.read())
216
219
  },
220
+ /**
221
+ * Updates session without revalidating the cart. Used when aligning
222
+ * `channel.salesChannel` to an orderForm SC adopted by `validateCart`.
223
+ * Still runs `validateSession` via the store's optimistic validator; that
224
+ * path keeps an explicit (non-default) client SC.
225
+ */
226
+ setSilent: (val: Session) => {
227
+ if (deepEqual(val, defaultStore.read()) === true) return
228
+
229
+ defaultStore.set(val)
230
+ },
217
231
  }
218
232
 
219
233
  export async function logoutAndClearSession(session: Session) {
@@ -0,0 +1,68 @@
1
+ import type { Session } from '@faststore/sdk'
2
+
3
+ function parseChannelRecord(
4
+ raw: string | null | undefined
5
+ ): Record<string, unknown> {
6
+ try {
7
+ const parsed: unknown = JSON.parse(raw || '{}')
8
+
9
+ if (
10
+ parsed !== null &&
11
+ typeof parsed === 'object' &&
12
+ !Array.isArray(parsed)
13
+ ) {
14
+ return parsed as Record<string, unknown>
15
+ }
16
+ } catch {
17
+ // Malformed channel — reset below.
18
+ }
19
+
20
+ return {}
21
+ }
22
+
23
+ function readSalesChannel(channel: Record<string, unknown>): string {
24
+ const value = channel.salesChannel
25
+
26
+ if (typeof value === 'string' || typeof value === 'number') {
27
+ return String(value)
28
+ }
29
+
30
+ return ''
31
+ }
32
+
33
+ /**
34
+ * Aligns `fs::session.channel` with a sales channel adopted from the orderForm
35
+ * (e.g. after Quick Order) without revalidating the cart.
36
+ *
37
+ * Uses `sessionStore.setSilent` so we do not trigger `cartStore.set` mid-flight
38
+ * (that race previously wiped SC-exclusive items). Marks the channel as
39
+ * explicit (`hasOnlyDefaultSalesChannel: false`) so `validateSession` keeps it.
40
+ */
41
+ export function syncSalesChannelFromOrderForm(
42
+ salesChannel: string | null | undefined,
43
+ readSession: () => Session,
44
+ setSilent: (session: Session) => void
45
+ ): boolean {
46
+ if (salesChannel == null || salesChannel === '') {
47
+ return false
48
+ }
49
+
50
+ const session = readSession()
51
+ const channel = parseChannelRecord(session.channel)
52
+ const nextSalesChannel = String(salesChannel)
53
+
54
+ if (readSalesChannel(channel) === nextSalesChannel) {
55
+ return false
56
+ }
57
+
58
+ setSilent({
59
+ ...session,
60
+ channel: JSON.stringify({
61
+ ...channel,
62
+ salesChannel: nextSalesChannel,
63
+ hasOnlyDefaultSalesChannel: false,
64
+ }),
65
+ })
66
+
67
+ return true
68
+ }
@@ -29,23 +29,17 @@ vi.mock('src/components/skeletons/ProductShelfSkeleton', () => ({
29
29
  }))
30
30
 
31
31
  const useRecommendations = vi.hoisted(() => vi.fn())
32
- vi.mock(
33
- 'src/components/sections/RecommendationShelf/useRecommendations',
34
- () => ({
35
- useRecommendations,
36
- })
37
- )
32
+ vi.mock('src/sdk/recommendations/useRecommendations', () => ({
33
+ useRecommendations,
34
+ }))
38
35
 
39
36
  const useScreenResize = vi.hoisted(() => vi.fn())
40
37
  vi.mock('src/sdk/ui/useScreenResize', () => ({ default: useScreenResize }))
41
38
 
42
39
  const useRecommendationUserId = vi.hoisted(() => vi.fn())
43
- vi.mock(
44
- 'src/components/sections/RecommendationShelf/useRecommendationUserId',
45
- () => ({
46
- useRecommendationUserId,
47
- })
48
- )
40
+ vi.mock('src/sdk/recommendations/useRecommendationUserId', () => ({
41
+ useRecommendationUserId,
42
+ }))
49
43
 
50
44
  import { RecommendationShelf } from 'src/components/sections/RecommendationShelf/RecommendationShelf'
51
45
 
@@ -0,0 +1,96 @@
1
+ import { createBaseStore } from '@faststore/sdk'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import {
4
+ SESSION_VALIDATION_WAIT_MS,
5
+ waitForSessionValidated,
6
+ } from '../../../src/sdk/cart/waitForSessionValidated'
7
+
8
+ describe('waitForSessionValidated', () => {
9
+ afterEach(() => {
10
+ vi.useRealTimers()
11
+ })
12
+
13
+ it('resolves immediately when session was already validated', async () => {
14
+ const store = createBaseStore(true)
15
+
16
+ await expect(waitForSessionValidated(store)).resolves.toBeUndefined()
17
+ })
18
+
19
+ it('resolves when session becomes validated after subscribe', async () => {
20
+ const store = createBaseStore(false)
21
+
22
+ const pending = waitForSessionValidated(store, 5_000)
23
+ store.set(true)
24
+
25
+ await expect(pending).resolves.toBeUndefined()
26
+ })
27
+
28
+ it('ignores subscribe callbacks while validated is still false', async () => {
29
+ vi.useFakeTimers()
30
+ const listeners: Array<(value: boolean) => void> = []
31
+ const store = {
32
+ read: vi.fn(() => false),
33
+ readInitial: vi.fn(() => false),
34
+ subscribe: vi.fn((listener: (value: boolean) => void) => {
35
+ listeners.push(listener)
36
+ return () => {
37
+ const index = listeners.indexOf(listener)
38
+ if (index >= 0) {
39
+ listeners.splice(index, 1)
40
+ }
41
+ }
42
+ }),
43
+ set: vi.fn(),
44
+ }
45
+
46
+ const pending = waitForSessionValidated(store, SESSION_VALIDATION_WAIT_MS)
47
+
48
+ // Emit false — should not resolve yet
49
+ for (const listener of listeners) {
50
+ listener(false)
51
+ }
52
+
53
+ await vi.advanceTimersByTimeAsync(SESSION_VALIDATION_WAIT_MS)
54
+ await expect(pending).resolves.toBeUndefined()
55
+ })
56
+
57
+ it('resolves on timeout when session never validates', async () => {
58
+ vi.useFakeTimers()
59
+ const store = createBaseStore(false)
60
+
61
+ const pending = waitForSessionValidated(store, SESSION_VALIDATION_WAIT_MS)
62
+
63
+ await vi.advanceTimersByTimeAsync(SESSION_VALIDATION_WAIT_MS)
64
+
65
+ await expect(pending).resolves.toBeUndefined()
66
+ })
67
+
68
+ it('resolves the race when validation finishes between read and subscribe', async () => {
69
+ let current = false
70
+ const listeners: Array<(value: boolean) => void> = []
71
+
72
+ const store = {
73
+ read: vi.fn(() => {
74
+ // First read: not validated. Second read (post-subscribe): validated.
75
+ const value = current
76
+ current = true
77
+ return value
78
+ }),
79
+ readInitial: vi.fn(() => false),
80
+ subscribe: vi.fn((listener: (value: boolean) => void) => {
81
+ listeners.push(listener)
82
+ return () => {
83
+ const index = listeners.indexOf(listener)
84
+ if (index >= 0) {
85
+ listeners.splice(index, 1)
86
+ }
87
+ }
88
+ }),
89
+ set: vi.fn(),
90
+ }
91
+
92
+ await expect(waitForSessionValidated(store, 5_000)).resolves.toBeUndefined()
93
+ expect(store.subscribe).toHaveBeenCalledTimes(1)
94
+ expect(store.read).toHaveBeenCalledTimes(2)
95
+ })
96
+ })
@@ -0,0 +1,199 @@
1
+ import { renderHook } from '@testing-library/react'
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ const usePDP = vi.hoisted(() => vi.fn())
5
+ vi.mock('@faststore/core', () => ({ usePDP }))
6
+
7
+ const useCart = vi.hoisted(() => vi.fn())
8
+ vi.mock('src/sdk/cart', () => ({ useCart }))
9
+
10
+ const useRecommendations = vi.hoisted(() => vi.fn())
11
+ vi.mock('src/sdk/recommendations/useRecommendations', () => ({
12
+ useRecommendations,
13
+ }))
14
+
15
+ const useRecommendationUserId = vi.hoisted(() => vi.fn())
16
+ vi.mock('src/sdk/recommendations/useRecommendationUserId', () => ({
17
+ useRecommendationUserId,
18
+ }))
19
+
20
+ import { useRecommendationShelf } from 'src/sdk/recommendations/useRecommendationShelf'
21
+
22
+ const TOP_ITEMS_VRN = 'vrn:recommendations:acc:rec-top-items-v2:campaign-1'
23
+ const CROSS_SELL_VRN = 'vrn:recommendations:acc:rec-cross-v2:campaign-1'
24
+
25
+ const cartItem = (productGroupID: string) => ({
26
+ itemOffered: { isVariantOf: { productGroupID } },
27
+ })
28
+
29
+ const campaignData = {
30
+ products: [
31
+ { id: 'p-1', isVariantOf: { productGroupID: 'p-1' } },
32
+ { id: 'p-2', isVariantOf: { productGroupID: 'p-2' } },
33
+ ],
34
+ correlationId: 'corr-1',
35
+ campaign: { id: 'camp-1', title: 'Recommended for you', type: 'TOP_ITEMS' },
36
+ }
37
+
38
+ const lastArgs = () => useRecommendations.mock.calls.at(-1)?.[0]
39
+
40
+ beforeEach(() => {
41
+ usePDP.mockReturnValue({ data: undefined })
42
+ useCart.mockReturnValue({ items: [] })
43
+ useRecommendationUserId.mockReturnValue('user-1')
44
+ useRecommendations.mockReturnValue({
45
+ data: campaignData,
46
+ isLoading: false,
47
+ error: null,
48
+ })
49
+ })
50
+
51
+ afterEach(() => {
52
+ vi.clearAllMocks()
53
+ })
54
+
55
+ describe('useRecommendationShelf', () => {
56
+ it('exposes the campaign payload when enabled', () => {
57
+ const { result } = renderHook(() =>
58
+ useRecommendationShelf({
59
+ campaignVrn: TOP_ITEMS_VRN,
60
+ enableRecommendations: true,
61
+ })
62
+ )
63
+
64
+ expect(result.current.items).toHaveLength(2)
65
+ expect(result.current.campaign?.title).toBe('Recommended for you')
66
+ expect(result.current.correlationId).toBe('corr-1')
67
+ })
68
+
69
+ it('never fetches while recommendations are disabled', () => {
70
+ renderHook(() => useRecommendationShelf({ campaignVrn: TOP_ITEMS_VRN }))
71
+
72
+ expect(lastArgs()).toBeNull()
73
+ })
74
+
75
+ it('never fetches before a userId resolves', () => {
76
+ useRecommendationUserId.mockReturnValue(null)
77
+
78
+ renderHook(() =>
79
+ useRecommendationShelf({
80
+ campaignVrn: TOP_ITEMS_VRN,
81
+ enableRecommendations: true,
82
+ })
83
+ )
84
+
85
+ expect(lastArgs()).toBeNull()
86
+ })
87
+
88
+ it('never fetches on a malformed campaign vrn', () => {
89
+ renderHook(() =>
90
+ useRecommendationShelf({
91
+ campaignVrn: 'not-a-vrn',
92
+ enableRecommendations: true,
93
+ })
94
+ )
95
+
96
+ expect(lastArgs()).toBeNull()
97
+ })
98
+
99
+ it('anchors cart-context campaigns on the deduplicated cart products', () => {
100
+ useCart.mockReturnValue({
101
+ items: [cartItem('pg-1'), cartItem('pg-2'), cartItem('pg-1')],
102
+ })
103
+
104
+ renderHook(() =>
105
+ useRecommendationShelf({
106
+ campaignVrn: CROSS_SELL_VRN,
107
+ enableRecommendations: true,
108
+ itemsContext: 'CART',
109
+ })
110
+ )
111
+
112
+ expect(lastArgs()).toEqual({
113
+ userId: 'user-1',
114
+ campaignVrn: CROSS_SELL_VRN,
115
+ products: ['pg-1', 'pg-2'],
116
+ })
117
+ })
118
+
119
+ it('skips cart-context campaigns while the cart is empty', () => {
120
+ renderHook(() =>
121
+ useRecommendationShelf({
122
+ campaignVrn: CROSS_SELL_VRN,
123
+ enableRecommendations: true,
124
+ itemsContext: 'CART',
125
+ })
126
+ )
127
+
128
+ expect(lastArgs()).toBeNull()
129
+ })
130
+
131
+ it('still fetches context-agnostic campaigns on an empty cart', () => {
132
+ renderHook(() =>
133
+ useRecommendationShelf({
134
+ campaignVrn: TOP_ITEMS_VRN,
135
+ enableRecommendations: true,
136
+ itemsContext: 'CART',
137
+ })
138
+ )
139
+
140
+ expect(lastArgs()).toEqual({
141
+ userId: 'user-1',
142
+ campaignVrn: TOP_ITEMS_VRN,
143
+ products: [],
144
+ })
145
+ })
146
+
147
+ it('follows the live cart context when not frozen', () => {
148
+ useCart.mockReturnValue({ items: [cartItem('pg-1')] })
149
+
150
+ const { rerender } = renderHook(() =>
151
+ useRecommendationShelf({
152
+ campaignVrn: CROSS_SELL_VRN,
153
+ enableRecommendations: true,
154
+ itemsContext: 'CART',
155
+ })
156
+ )
157
+
158
+ useCart.mockReturnValue({ items: [cartItem('pg-1'), cartItem('pg-2')] })
159
+ rerender()
160
+
161
+ expect(lastArgs()?.products).toEqual(['pg-1', 'pg-2'])
162
+ })
163
+
164
+ it('anchors PDP-context campaigns on the current product', () => {
165
+ usePDP.mockReturnValue({
166
+ data: { product: { isVariantOf: { productGroupID: 'pg-pdp' } } },
167
+ })
168
+
169
+ renderHook(() =>
170
+ useRecommendationShelf({
171
+ campaignVrn: CROSS_SELL_VRN,
172
+ enableRecommendations: true,
173
+ })
174
+ )
175
+
176
+ expect(lastArgs()?.products).toEqual(['pg-pdp'])
177
+ })
178
+
179
+ it('returns no items and logs without the userId on error', () => {
180
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
181
+ useRecommendations.mockReturnValue({
182
+ data: undefined,
183
+ isLoading: false,
184
+ error: new Error('boom'),
185
+ })
186
+
187
+ const { result } = renderHook(() =>
188
+ useRecommendationShelf({
189
+ campaignVrn: TOP_ITEMS_VRN,
190
+ enableRecommendations: true,
191
+ })
192
+ )
193
+
194
+ expect(result.current.items).toEqual([])
195
+ expect(result.current.error).toBeInstanceOf(Error)
196
+ expect(consoleError).toHaveBeenCalled()
197
+ expect(JSON.stringify(consoleError.mock.calls)).not.toContain('user-1')
198
+ })
199
+ })
@@ -14,7 +14,7 @@ vi.mock('src/utils/retry', () => ({
14
14
  retry: (fn: () => unknown) => Promise.resolve().then(fn),
15
15
  }))
16
16
 
17
- import { useRecommendationUserId } from 'src/components/sections/RecommendationShelf/useRecommendationUserId'
17
+ import { useRecommendationUserId } from 'src/sdk/recommendations/useRecommendationUserId'
18
18
 
19
19
  const CAMPAIGN_VRN = 'vrn:recommendations:acc:rec-top-items-v2:campaign-1'
20
20
 
@@ -7,7 +7,7 @@ vi.mock('src/sdk/graphql/useQuery', () => ({
7
7
  useQuery: useQueryMock,
8
8
  }))
9
9
 
10
- import { useRecommendations } from 'src/components/sections/RecommendationShelf/useRecommendations'
10
+ import { useRecommendations } from 'src/sdk/recommendations/useRecommendations'
11
11
 
12
12
  afterEach(() => {
13
13
  vi.clearAllMocks()
@@ -1,9 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
- import {
4
- getTypeFromVrn,
5
- isValidVrn,
6
- } from 'src/components/sections/RecommendationShelf/vrn'
3
+ import { getTypeFromVrn, isValidVrn } from 'src/sdk/recommendations/vrn'
7
4
 
8
5
  const vrn = (type: string) => `vrn:recommendations:account:${type}:campaign-1`
9
6