@faststore/core 4.6.0 → 4.7.0-dev.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.
Files changed (32) hide show
  1. package/.turbo/turbo-generate.log +4 -4
  2. package/.turbo/turbo-test.log +105 -90
  3. package/@generated/cached-operations.json +1 -0
  4. package/@generated/gql.ts +6 -0
  5. package/@generated/graphql.ts +61 -0
  6. package/@generated/persisted-documents.json +1 -0
  7. package/@generated/schema.graphql +56 -0
  8. package/CHANGELOG.md +10 -0
  9. package/cms/faststore/my-account/components/cms_component__accountlistcards.jsonc +90 -0
  10. package/cms/faststore/my-account/pages/cms_content_type__myaccountcards.jsonc +18 -0
  11. package/package.json +6 -6
  12. package/public/icons.svg +1 -1
  13. package/src/components/account/Layout/Layout.tsx +4 -0
  14. package/src/components/account/cards/MyAccountListCards/MyAccountListCards.tsx +409 -0
  15. package/src/components/account/cards/MyAccountListCards/index.ts +11 -0
  16. package/src/components/account/cards/MyAccountListCards/myAccountListCardsLabels.ts +44 -0
  17. package/src/components/account/cards/MyAccountListCards/styles.module.scss +126 -0
  18. package/src/components/cms/account/Components.ts +2 -0
  19. package/src/components/sections/Account/AccountListCards.tsx +69 -0
  20. package/src/pages/pvt/account/cards/index.tsx +244 -0
  21. package/src/sdk/account/accountPageContext.ts +16 -0
  22. package/src/sdk/account/getB2BSessionClaims.ts +42 -0
  23. package/src/sdk/account/getMyAccountRoutes.ts +7 -0
  24. package/src/server/cms/myAccountDefaultSections.ts +2 -0
  25. package/src/utils/getCookie.ts +8 -1
  26. package/test/components/account/cards/MyAccountListCards.browser.test.tsx +301 -0
  27. package/test/components/sections/Account/AccountListCards.browser.test.tsx +57 -0
  28. package/test/pages/pvt/account/cards/index.test.ts +202 -0
  29. package/test/sdk/account/getB2BSessionClaims.test.ts +48 -0
  30. package/test/sdk/account/getMyAccountRoutes.test.ts +10 -0
  31. package/test/server/index.test.ts +2 -0
  32. package/test/utils/getCookie.test.ts +23 -0
@@ -0,0 +1,301 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react'
3
+ import { afterEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ const mockReload = vi.hoisted(() => vi.fn())
6
+ const mockUseRouter = vi.hoisted(() => vi.fn(() => ({ reload: mockReload })))
7
+
8
+ vi.mock('next/router', () => ({
9
+ useRouter: mockUseRouter,
10
+ }))
11
+
12
+ import { MyAccountListCards } from 'src/components/account/cards/MyAccountListCards'
13
+
14
+ afterEach(() => {
15
+ cleanup()
16
+ mockReload.mockClear()
17
+ })
18
+
19
+ const VISA_PERSONAL = {
20
+ accountId: 'acc-1',
21
+ bin: '411111',
22
+ cardNumber: '**** **** **** 1111',
23
+ paymentSystem: '2',
24
+ paymentSystemName: 'Visa',
25
+ isDefault: true,
26
+ isActive: true,
27
+ }
28
+
29
+ const MASTERCARD_PERSONAL = {
30
+ accountId: 'acc-2',
31
+ bin: '555555',
32
+ cardNumber: '**** **** **** 2222',
33
+ paymentSystem: '3',
34
+ paymentSystemName: 'Mastercard',
35
+ isDefault: false,
36
+ isActive: true,
37
+ }
38
+
39
+ const AMEX_SHARED = {
40
+ accountId: 'acc-3',
41
+ bin: '378282',
42
+ cardNumber: '**** **** **** 3333',
43
+ paymentSystem: '4',
44
+ paymentSystemName: 'Amex',
45
+ isDefault: false,
46
+ isActive: true,
47
+ }
48
+
49
+ describe('MyAccountListCards', () => {
50
+ it('shows the tab bar and renders Personal cards by default', () => {
51
+ render(
52
+ <MyAccountListCards
53
+ personalCards={[VISA_PERSONAL, MASTERCARD_PERSONAL]}
54
+ sharedCards={[AMEX_SHARED]}
55
+ hasOrgAssociation
56
+ canViewPersonalCards
57
+ />
58
+ )
59
+
60
+ expect(screen.getByRole('tablist')).toBeInTheDocument()
61
+ expect(screen.getByRole('tab', { name: 'Personal' })).toHaveAttribute(
62
+ 'aria-selected',
63
+ 'true'
64
+ )
65
+ expect(screen.getByText('Visa')).toBeInTheDocument()
66
+ expect(screen.getByText('Mastercard')).toBeInTheDocument()
67
+ expect(screen.queryByText('Amex')).not.toBeInTheDocument()
68
+ })
69
+
70
+ it('marks the default card with the default badge', () => {
71
+ render(
72
+ <MyAccountListCards
73
+ personalCards={[VISA_PERSONAL, MASTERCARD_PERSONAL]}
74
+ sharedCards={[]}
75
+ hasOrgAssociation={false}
76
+ canViewPersonalCards
77
+ />
78
+ )
79
+
80
+ const visaItem = screen
81
+ .getByText('Visa')
82
+ .closest('[data-fs-list-cards-item]')
83
+ expect(visaItem).toHaveTextContent('Default')
84
+
85
+ const mastercardItem = screen
86
+ .getByText('Mastercard')
87
+ .closest('[data-fs-list-cards-item]')
88
+ expect(mastercardItem).not.toHaveTextContent('Default')
89
+ })
90
+
91
+ it('falls back to the generic card label when paymentSystemName is absent', () => {
92
+ render(
93
+ <MyAccountListCards
94
+ personalCards={[{ accountId: 'acc-9', cardNumber: '**** 9999' }]}
95
+ sharedCards={[]}
96
+ hasOrgAssociation={false}
97
+ canViewPersonalCards
98
+ />
99
+ )
100
+
101
+ expect(screen.getByText('Card')).toBeInTheDocument()
102
+ })
103
+
104
+ it('prefers cardLabel over paymentSystemName when the service has a nickname on file', () => {
105
+ render(
106
+ <MyAccountListCards
107
+ personalCards={[{ ...VISA_PERSONAL, cardLabel: 'Team lunch card' }]}
108
+ sharedCards={[]}
109
+ hasOrgAssociation={false}
110
+ canViewPersonalCards
111
+ />
112
+ )
113
+
114
+ expect(screen.getByText('Team lunch card')).toBeInTheDocument()
115
+ expect(screen.queryByText('Visa')).not.toBeInTheDocument()
116
+ })
117
+
118
+ it('matches cardLabel when searching', () => {
119
+ render(
120
+ <MyAccountListCards
121
+ personalCards={[
122
+ { ...VISA_PERSONAL, cardLabel: 'Team lunch card' },
123
+ MASTERCARD_PERSONAL,
124
+ ]}
125
+ sharedCards={[]}
126
+ hasOrgAssociation={false}
127
+ canViewPersonalCards
128
+ />
129
+ )
130
+
131
+ fireEvent.change(screen.getByPlaceholderText('Search'), {
132
+ target: { value: 'lunch' },
133
+ })
134
+
135
+ expect(screen.getByText('Team lunch card')).toBeInTheDocument()
136
+ expect(screen.queryByText('Mastercard')).not.toBeInTheDocument()
137
+ })
138
+
139
+ it('switches to the Shared tab by clicking it, and shows the tooltip info icon', () => {
140
+ render(
141
+ <MyAccountListCards
142
+ personalCards={[VISA_PERSONAL]}
143
+ sharedCards={[AMEX_SHARED]}
144
+ hasOrgAssociation
145
+ canViewPersonalCards
146
+ />
147
+ )
148
+
149
+ fireEvent.click(screen.getByRole('tab', { name: 'Shared' }))
150
+
151
+ expect(screen.getByRole('tab', { name: 'Shared' })).toHaveAttribute(
152
+ 'aria-selected',
153
+ 'true'
154
+ )
155
+ expect(screen.getByText('Amex')).toBeInTheDocument()
156
+ expect(screen.queryByText('Visa')).not.toBeInTheDocument()
157
+ })
158
+
159
+ it('moves focus and switches tabs on ArrowRight/ArrowLeft', () => {
160
+ render(
161
+ <MyAccountListCards
162
+ personalCards={[VISA_PERSONAL]}
163
+ sharedCards={[AMEX_SHARED]}
164
+ hasOrgAssociation
165
+ canViewPersonalCards
166
+ />
167
+ )
168
+
169
+ const personalTab = screen.getByRole('tab', { name: 'Personal' })
170
+ const sharedTab = screen.getByRole('tab', { name: 'Shared' })
171
+
172
+ personalTab.focus()
173
+ fireEvent.keyDown(personalTab, { key: 'ArrowRight' })
174
+ expect(sharedTab).toHaveAttribute('aria-selected', 'true')
175
+ expect(sharedTab).toHaveFocus()
176
+
177
+ fireEvent.keyDown(sharedTab, { key: 'ArrowLeft' })
178
+ expect(personalTab).toHaveAttribute('aria-selected', 'true')
179
+ expect(personalTab).toHaveFocus()
180
+ })
181
+
182
+ it('renders shared cards directly with no tab chrome when Personal is unavailable (sharedOnly)', () => {
183
+ render(
184
+ <MyAccountListCards
185
+ personalCards={[]}
186
+ sharedCards={[AMEX_SHARED]}
187
+ hasOrgAssociation
188
+ canViewPersonalCards={false}
189
+ />
190
+ )
191
+
192
+ expect(screen.queryByRole('tablist')).not.toBeInTheDocument()
193
+ expect(screen.getByText('Amex')).toBeInTheDocument()
194
+ })
195
+
196
+ it('renders only the Personal tab when there is no org association (unconfirmed combo kept as-is)', () => {
197
+ render(
198
+ <MyAccountListCards
199
+ personalCards={[VISA_PERSONAL]}
200
+ sharedCards={[]}
201
+ hasOrgAssociation={false}
202
+ canViewPersonalCards={false}
203
+ />
204
+ )
205
+
206
+ expect(screen.getByRole('tablist')).toBeInTheDocument()
207
+ expect(
208
+ screen.queryByRole('tab', { name: 'Shared' })
209
+ ).not.toBeInTheDocument()
210
+ expect(screen.getByRole('tab', { name: 'Personal' })).toBeInTheDocument()
211
+ })
212
+
213
+ it('filters cards by search term', () => {
214
+ render(
215
+ <MyAccountListCards
216
+ personalCards={[VISA_PERSONAL, MASTERCARD_PERSONAL]}
217
+ sharedCards={[]}
218
+ hasOrgAssociation={false}
219
+ canViewPersonalCards
220
+ />
221
+ )
222
+
223
+ fireEvent.change(screen.getByPlaceholderText('Search'), {
224
+ target: { value: 'visa' },
225
+ })
226
+
227
+ expect(screen.getByText('Visa')).toBeInTheDocument()
228
+ expect(screen.queryByText('Mastercard')).not.toBeInTheDocument()
229
+ })
230
+
231
+ it('shows the no-results empty state when the search matches nothing', () => {
232
+ render(
233
+ <MyAccountListCards
234
+ personalCards={[VISA_PERSONAL]}
235
+ sharedCards={[]}
236
+ hasOrgAssociation={false}
237
+ canViewPersonalCards
238
+ />
239
+ )
240
+
241
+ fireEvent.change(screen.getByPlaceholderText('Search'), {
242
+ target: { value: 'nonexistent-brand' },
243
+ })
244
+
245
+ expect(screen.getByText('No results found')).toBeInTheDocument()
246
+ })
247
+
248
+ it('shows the empty state when there are no cards at all', () => {
249
+ render(
250
+ <MyAccountListCards
251
+ personalCards={[]}
252
+ sharedCards={[]}
253
+ hasOrgAssociation={false}
254
+ canViewPersonalCards
255
+ />
256
+ )
257
+
258
+ expect(screen.getByText("You don't have any cards")).toBeInTheDocument()
259
+ })
260
+
261
+ it('paginates when there are more than 25 cards', () => {
262
+ const manyCards = Array.from({ length: 26 }, (_, i) => ({
263
+ accountId: `acc-${i}`,
264
+ paymentSystemName: `Brand ${i}`,
265
+ cardNumber: `**** ${i}`,
266
+ }))
267
+
268
+ render(
269
+ <MyAccountListCards
270
+ personalCards={manyCards}
271
+ sharedCards={[]}
272
+ hasOrgAssociation={false}
273
+ canViewPersonalCards
274
+ />
275
+ )
276
+
277
+ expect(screen.getByText('Brand 0')).toBeInTheDocument()
278
+ expect(screen.queryByText('Brand 25')).not.toBeInTheDocument()
279
+
280
+ fireEvent.click(screen.getByRole('button', { name: 'Next Page' }))
281
+
282
+ expect(screen.getByText('Brand 25')).toBeInTheDocument()
283
+ expect(screen.queryByText('Brand 0')).not.toBeInTheDocument()
284
+ })
285
+
286
+ it('shows the error state with a retry action that reloads the page', () => {
287
+ render(
288
+ <MyAccountListCards
289
+ personalCards={[]}
290
+ sharedCards={[]}
291
+ hasOrgAssociation={false}
292
+ canViewPersonalCards
293
+ hasError
294
+ />
295
+ )
296
+
297
+ expect(screen.getByText("We couldn't load your cards")).toBeInTheDocument()
298
+ fireEvent.click(screen.getByRole('button', { name: 'Try again' }))
299
+ expect(mockReload).toHaveBeenCalledTimes(1)
300
+ })
301
+ })
@@ -0,0 +1,57 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+ import { cleanup, render, screen } from '@testing-library/react'
3
+ import { afterEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ const mockUseAccountPageData = vi.hoisted(() => vi.fn())
6
+ const mockUseRouter = vi.hoisted(() => vi.fn(() => ({ reload: vi.fn() })))
7
+
8
+ vi.mock('next/router', () => ({ useRouter: mockUseRouter }))
9
+ vi.mock('src/sdk/account/accountPageContext', () => ({
10
+ useAccountPageData: mockUseAccountPageData,
11
+ }))
12
+
13
+ import AccountListCards from 'src/components/sections/Account/AccountListCards'
14
+
15
+ afterEach(cleanup)
16
+
17
+ describe('AccountListCards', () => {
18
+ it('reads accountPageData and forwards it to MyAccountListCards', () => {
19
+ mockUseAccountPageData.mockReturnValueOnce({
20
+ personalCards: [{ accountId: 'acc-1', paymentSystemName: 'Visa' }],
21
+ sharedCards: [],
22
+ hasOrgAssociation: false,
23
+ canViewPersonalCards: true,
24
+ hasError: false,
25
+ })
26
+
27
+ render(<AccountListCards />)
28
+
29
+ expect(screen.getByText('Visa')).toBeInTheDocument()
30
+ })
31
+
32
+ it('falls back to empty card arrays when the page data omits them', () => {
33
+ mockUseAccountPageData.mockReturnValueOnce({
34
+ hasOrgAssociation: false,
35
+ canViewPersonalCards: true,
36
+ hasError: false,
37
+ })
38
+
39
+ render(<AccountListCards />)
40
+
41
+ expect(screen.getByText("You don't have any cards")).toBeInTheDocument()
42
+ })
43
+
44
+ it('applies CMS-provided label overrides', () => {
45
+ mockUseAccountPageData.mockReturnValueOnce({
46
+ personalCards: [],
47
+ sharedCards: [],
48
+ hasOrgAssociation: false,
49
+ canViewPersonalCards: true,
50
+ hasError: false,
51
+ })
52
+
53
+ render(<AccountListCards pageTitle="Meus Cartões" />)
54
+
55
+ expect(screen.getByText('Meus Cartões')).toBeInTheDocument()
56
+ })
57
+ })
@@ -0,0 +1,202 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ const mockExecute = vi.hoisted(() => vi.fn())
4
+ const mockGetB2BSessionClaims = vi.hoisted(() => vi.fn())
5
+ const mockGetIsRepresentative = vi.hoisted(() => vi.fn(() => true))
6
+ const mockGetMyAccountRedirect = vi.hoisted(() =>
7
+ vi.fn(() => ({ isFaststoreMyAccountEnabled: true, redirect: undefined }))
8
+ )
9
+
10
+ vi.mock('src/server', () => ({ execute: mockExecute }))
11
+ vi.mock('src/server/cms/global', () => ({
12
+ injectGlobalSections: vi.fn(() => ({ sections: [], settings: {} })),
13
+ }))
14
+ vi.mock('src/server/cms/myAccountDefaultSections', () => ({
15
+ extractAccountNavigationData: vi.fn(() => ({
16
+ pageSections: [],
17
+ navigationData: {},
18
+ })),
19
+ }))
20
+ vi.mock('src/server/cms/fetchMyAccountPageContent', () => ({
21
+ fetchMyAccountPageContent: vi.fn(async () => ({ sections: [] })),
22
+ }))
23
+ vi.mock('src/components/cms/GlobalSections', () => ({
24
+ getGlobalSectionsData: vi.fn(() => [
25
+ Promise.resolve(null),
26
+ Promise.resolve(null),
27
+ Promise.resolve(null),
28
+ ]),
29
+ }))
30
+ vi.mock('src/sdk/account/getB2BSessionClaims', () => ({
31
+ getB2BSessionClaims: mockGetB2BSessionClaims,
32
+ }))
33
+ vi.mock('src/sdk/account/getIsRepresentative', () => ({
34
+ getIsRepresentative: mockGetIsRepresentative,
35
+ }))
36
+ vi.mock('src/utils/myAccountRedirect', () => ({
37
+ getMyAccountRedirect: mockGetMyAccountRedirect,
38
+ }))
39
+
40
+ import { getServerSideProps } from 'src/pages/pvt/account/cards/index'
41
+
42
+ function makeContext() {
43
+ return {
44
+ req: { headers: {} },
45
+ query: {},
46
+ previewData: undefined,
47
+ locale: undefined,
48
+ } as any
49
+ }
50
+
51
+ const CARD_PERSONAL = { accountId: 'p1', origin: 'personal' }
52
+ const CARD_SHARED = { accountId: 's1', origin: 'shared' }
53
+
54
+ function mockSuccessfulExecute({
55
+ cards = [CARD_PERSONAL, CARD_SHARED],
56
+ hasAdHocCardAccess = true,
57
+ accountName = 'Jane Buyer',
58
+ } = {}) {
59
+ mockExecute.mockResolvedValueOnce({
60
+ data: {
61
+ listCreditCards: { list: cards },
62
+ accountProfile: { name: accountName },
63
+ hasAdHocCardAccess,
64
+ },
65
+ errors: undefined,
66
+ })
67
+ }
68
+
69
+ describe('Cards page getServerSideProps', () => {
70
+ it('redirects when the FastStore My Account experience is disabled', async () => {
71
+ mockGetMyAccountRedirect.mockReturnValueOnce({
72
+ isFaststoreMyAccountEnabled: false,
73
+ redirect: { destination: '/account', permanent: false },
74
+ })
75
+ mockGetB2BSessionClaims.mockReturnValueOnce({
76
+ hasOrgAssociation: false,
77
+ hasCustomerId: false,
78
+ })
79
+
80
+ const result = await getServerSideProps(makeContext())
81
+
82
+ expect(result).toEqual({
83
+ redirect: { destination: '/account', permanent: false },
84
+ })
85
+ expect(mockExecute).not.toHaveBeenCalled()
86
+ })
87
+
88
+ it('redirects to /pvt/account/403 when the query fails with 401/403', async () => {
89
+ mockGetB2BSessionClaims.mockReturnValueOnce({
90
+ hasOrgAssociation: false,
91
+ hasCustomerId: false,
92
+ })
93
+ mockExecute.mockResolvedValueOnce({
94
+ data: undefined,
95
+ errors: [{ extensions: { status: 401 } }],
96
+ })
97
+
98
+ const result: any = await getServerSideProps(makeContext())
99
+
100
+ expect(result.redirect.destination).toContain('/pvt/account/403')
101
+ })
102
+
103
+ it('renders the error state (no redirect) when the query fails with a non-auth status', async () => {
104
+ mockGetB2BSessionClaims.mockReturnValueOnce({
105
+ hasOrgAssociation: false,
106
+ hasCustomerId: false,
107
+ })
108
+ mockExecute.mockResolvedValueOnce({
109
+ data: undefined,
110
+ errors: [{ extensions: { status: 502 } }],
111
+ })
112
+
113
+ const result: any = await getServerSideProps(makeContext())
114
+
115
+ expect(result.redirect).toBeUndefined()
116
+ expect(result.props.accountPageData.hasError).toBe(true)
117
+ expect(result.props.accountPageData.personalCards).toEqual([])
118
+ expect(result.props.accountPageData.sharedCards).toEqual([])
119
+ })
120
+
121
+ it('partitions the merged card list by origin and computes canViewPersonalCards', async () => {
122
+ mockGetB2BSessionClaims.mockReturnValueOnce({
123
+ hasOrgAssociation: true,
124
+ hasCustomerId: true,
125
+ })
126
+ mockSuccessfulExecute()
127
+
128
+ const result: any = await getServerSideProps(makeContext())
129
+
130
+ expect(result.props.accountPageData.personalCards).toEqual([CARD_PERSONAL])
131
+ expect(result.props.accountPageData.sharedCards).toEqual([CARD_SHARED])
132
+ expect(result.props.accountPageData.canViewPersonalCards).toBe(true)
133
+ expect(result.props.accountPageData.hasOrgAssociation).toBe(true)
134
+ expect(result.props.accountName).toBe('Jane Buyer')
135
+ expect(result.props).not.toHaveProperty('hasAdHocCardAccess')
136
+ })
137
+
138
+ it('never gates the route: canViewPersonalCards is false but no redirect happens', async () => {
139
+ mockGetB2BSessionClaims.mockReturnValueOnce({
140
+ hasOrgAssociation: true,
141
+ hasCustomerId: false,
142
+ })
143
+ mockSuccessfulExecute({ hasAdHocCardAccess: false })
144
+
145
+ const result: any = await getServerSideProps(makeContext())
146
+
147
+ expect(result.redirect).toBeUndefined()
148
+ expect(result.props.accountPageData.canViewPersonalCards).toBe(false)
149
+ })
150
+
151
+ it('drops personal cards from the SSR payload in the sharedOnly combination (hasOrgAssociation, canViewPersonalCards false)', async () => {
152
+ mockGetB2BSessionClaims.mockReturnValueOnce({
153
+ hasOrgAssociation: true,
154
+ hasCustomerId: false,
155
+ })
156
+ mockSuccessfulExecute({ hasAdHocCardAccess: false })
157
+
158
+ const result: any = await getServerSideProps(makeContext())
159
+
160
+ // The UI hides the Personal tab only for this combination (sharedOnly);
161
+ // this asserts the data itself never reaches __NEXT_DATA__ either (PR
162
+ // review, REQ-6's "hide" intent) — not just that the tab is visually
163
+ // hidden.
164
+ expect(result.props.accountPageData.personalCards).toEqual([])
165
+ expect(result.props.accountPageData.sharedCards).toEqual([CARD_SHARED])
166
+ })
167
+
168
+ it('keeps personal cards in the payload when there is no org association, even though canViewPersonalCards is false (FR-7: B2C buyers are never subject to this check)', async () => {
169
+ mockGetB2BSessionClaims.mockReturnValueOnce({
170
+ hasOrgAssociation: false,
171
+ hasCustomerId: false,
172
+ })
173
+ mockSuccessfulExecute({ hasAdHocCardAccess: false })
174
+
175
+ const result: any = await getServerSideProps(makeContext())
176
+
177
+ // Without hasOrgAssociation, the component still renders the Personal
178
+ // tab (the "kept as-is" O1 combination) — stripping the data here would
179
+ // silently regress a buyer this gate was never meant to touch, even
180
+ // though canViewPersonalCards itself is false.
181
+ expect(result.props.accountPageData.personalCards).toEqual([CARD_PERSONAL])
182
+ })
183
+
184
+ it('defaults hasAdHocCardAccess to allowed when the query field is missing', async () => {
185
+ mockGetB2BSessionClaims.mockReturnValueOnce({
186
+ hasOrgAssociation: true,
187
+ hasCustomerId: true,
188
+ })
189
+ mockExecute.mockResolvedValueOnce({
190
+ data: {
191
+ listCreditCards: { list: [] },
192
+ accountProfile: { name: 'Jane' },
193
+ hasAdHocCardAccess: undefined,
194
+ },
195
+ errors: undefined,
196
+ })
197
+
198
+ const result: any = await getServerSideProps(makeContext())
199
+
200
+ expect(result.props.accountPageData.canViewPersonalCards).toBe(true)
201
+ })
202
+ })
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getB2BSessionClaims } from '../../../src/sdk/account/getB2BSessionClaims'
3
+
4
+ const ACCOUNT = 'storeaccount'
5
+
6
+ const makeJwt = (payload: Record<string, unknown>) =>
7
+ `header.${Buffer.from(JSON.stringify(payload)).toString('base64')}.signature`
8
+
9
+ describe('getB2BSessionClaims', () => {
10
+ it('reads hasOrgAssociation and hasCustomerId from the token claims', () => {
11
+ const cookie = `VtexIdclientAutCookie_${ACCOUNT}=${makeJwt({ unitId: 'unit-1', customerId: 'customer-1' })}`
12
+
13
+ const result = getB2BSessionClaims({
14
+ headers: { cookie },
15
+ account: ACCOUNT,
16
+ })
17
+
18
+ expect(result).toEqual({ hasOrgAssociation: true, hasCustomerId: true })
19
+ })
20
+
21
+ it('returns false claims when the token has neither unitId nor customerId', () => {
22
+ const cookie = `VtexIdclientAutCookie_${ACCOUNT}=${makeJwt({})}`
23
+
24
+ const result = getB2BSessionClaims({
25
+ headers: { cookie },
26
+ account: ACCOUNT,
27
+ })
28
+
29
+ expect(result).toEqual({ hasOrgAssociation: false, hasCustomerId: false })
30
+ })
31
+
32
+ it('returns false claims (not a throw) when there is no auth cookie at all', () => {
33
+ const result = getB2BSessionClaims({ headers: {}, account: ACCOUNT })
34
+
35
+ expect(result).toEqual({ hasOrgAssociation: false, hasCustomerId: false })
36
+ })
37
+
38
+ it('does not throw when the auth cookie is malformed', () => {
39
+ const cookie = `VtexIdclientAutCookie_${ACCOUNT}=not-a-jwt`
40
+
41
+ const result = getB2BSessionClaims({
42
+ headers: { cookie },
43
+ account: ACCOUNT,
44
+ })
45
+
46
+ expect(result).toEqual({ hasOrgAssociation: false, hasCustomerId: false })
47
+ })
48
+ })
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import {
4
+ CARDS_ROUTE,
4
5
  ORDERS_ROUTE,
5
6
  PROFILE_ROUTE,
6
7
  QUOTES_ROUTE,
@@ -20,6 +21,7 @@ describe('getMyAccountRoutes', () => {
20
21
  expect(result.map((r) => r.route)).toEqual([
21
22
  PROFILE_ROUTE,
22
23
  ORDERS_ROUTE,
24
+ CARDS_ROUTE,
23
25
  QUOTES_ROUTE,
24
26
  USER_DETAILS_ROUTE,
25
27
  SECURITY_ROUTE,
@@ -27,6 +29,14 @@ describe('getMyAccountRoutes', () => {
27
29
  ])
28
30
  })
29
31
 
32
+ it('keeps routes with /pvt prefix unchanged', () => {
33
+ const result = getMyAccountRoutes({
34
+ routes: [{ title: 'Custom', route: '/pvt/custom' }],
35
+ })
36
+
37
+ expect(result.find((r) => r.title === 'Custom')?.route).toBe('/pvt/custom')
38
+ })
39
+
30
40
  it('prefixes routes with /pvt when missing', () => {
31
41
  const result = getMyAccountRoutes({
32
42
  routes: [{ title: 'Custom', route: '/custom' }],
@@ -73,6 +73,8 @@ const QUERIES = [
73
73
  'profile',
74
74
  'productCount',
75
75
  'userOrder',
76
+ 'listCreditCards',
77
+ 'hasAdHocCardAccess',
76
78
  'listUserOrders',
77
79
  'listUserQuotes',
78
80
  'userDetails',
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { parseJwt } from '../../src/utils/getCookie'
3
+
4
+ describe('parseJwt', () => {
5
+ it('returns null for an empty token', () => {
6
+ expect(parseJwt('')).toBeNull()
7
+ })
8
+
9
+ it('decodes a well-formed token payload', () => {
10
+ const payload = { unitId: 'unit-1' }
11
+ const token = `header.${Buffer.from(JSON.stringify(payload)).toString('base64')}.signature`
12
+
13
+ expect(parseJwt(token)).toEqual(payload)
14
+ })
15
+
16
+ it('returns null instead of throwing for a malformed token', () => {
17
+ expect(parseJwt('not-a-jwt')).toBeNull()
18
+ })
19
+
20
+ it('returns null instead of throwing when the payload segment is not valid base64/JSON', () => {
21
+ expect(parseJwt('header.%%%not-base64%%%.signature')).toBeNull()
22
+ })
23
+ })