@faststore/api 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 (36) hide show
  1. package/dist/cjs/index.js +51 -45
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/es/index.mjs +1345 -1237
  4. package/dist/es/index.mjs.map +1 -1
  5. package/dist/src/__generated__/schema.d.ts +6 -0
  6. package/dist/src/__generated__/schema.d.ts.map +1 -1
  7. package/dist/src/platforms/vtex/clients/commerce/index.d.ts +8 -1
  8. package/dist/src/platforms/vtex/clients/commerce/index.d.ts.map +1 -1
  9. package/dist/src/platforms/vtex/clients/index.d.ts +2 -1
  10. package/dist/src/platforms/vtex/clients/index.d.ts.map +1 -1
  11. package/dist/src/platforms/vtex/index.d.ts +1 -0
  12. package/dist/src/platforms/vtex/index.d.ts.map +1 -1
  13. package/dist/src/platforms/vtex/resolvers/getOrderEntryOperation.d.ts.map +1 -1
  14. package/dist/src/platforms/vtex/resolvers/index.d.ts +1 -0
  15. package/dist/src/platforms/vtex/resolvers/index.d.ts.map +1 -1
  16. package/dist/src/platforms/vtex/resolvers/mutation.d.ts +1 -0
  17. package/dist/src/platforms/vtex/resolvers/mutation.d.ts.map +1 -1
  18. package/dist/src/platforms/vtex/resolvers/query.d.ts.map +1 -1
  19. package/dist/src/platforms/vtex/resolvers/validateCart.d.ts +1 -0
  20. package/dist/src/platforms/vtex/resolvers/validateCart.d.ts.map +1 -1
  21. package/dist/src/platforms/vtex/resolvers/validateSession.d.ts.map +1 -1
  22. package/dist/src/platforms/vtex/utils/cartSalesChannel.d.ts +11 -0
  23. package/dist/src/platforms/vtex/utils/cartSalesChannel.d.ts.map +1 -0
  24. package/dist/src/platforms/vtex/utils/sessionChannel.d.ts +10 -0
  25. package/dist/src/platforms/vtex/utils/sessionChannel.d.ts.map +1 -0
  26. package/dist/src/platforms/vtex/utils/validateSessionHelpers.d.ts +88 -0
  27. package/dist/src/platforms/vtex/utils/validateSessionHelpers.d.ts.map +1 -0
  28. package/package.json +3 -3
  29. package/src/__generated__/schema.ts +6 -0
  30. package/src/platforms/vtex/clients/commerce/index.ts +17 -2
  31. package/src/platforms/vtex/resolvers/validateCart.ts +86 -8
  32. package/src/platforms/vtex/resolvers/validateSession.ts +63 -140
  33. package/src/platforms/vtex/typeDefs/order.graphql +6 -0
  34. package/src/platforms/vtex/utils/cartSalesChannel.ts +31 -0
  35. package/src/platforms/vtex/utils/sessionChannel.ts +32 -0
  36. package/src/platforms/vtex/utils/validateSessionHelpers.ts +202 -0
@@ -3,39 +3,29 @@ import deepEquals from 'fast-deep-equal'
3
3
  import type { GraphqlContext } from '..'
4
4
  import type {
5
5
  MutationValidateSessionArgs,
6
- StoreMarketingData,
7
6
  StoreSession,
8
7
  } from '../../../__generated__/schema'
9
8
  import ChannelMarshal from '../utils/channel'
10
- import { resolveActiveContractDisplayName } from '../utils/contract'
11
- import { getAuthCookie, parseJwt } from '../utils/cookies'
12
-
13
- async function getPreciseLocationData(
14
- clients: GraphqlContext['clients'],
15
- country: string,
16
- postalCode: string
17
- ) {
18
- try {
19
- const address = await clients.commerce.checkout.address({
20
- postalCode,
21
- country,
22
- })
23
-
24
- const geoCoordinates = address.geoCoordinates
25
- ? {
26
- latitude: address.geoCoordinates[1],
27
- longitude: address.geoCoordinates[0],
28
- }
29
- : null
30
-
31
- return { city: address.city, geoCoordinates }
32
- } catch (err) {
33
- console.error(
34
- `Error while getting geo coordinates for the current postal code (${postalCode}) and country (${country}).\n`
35
- )
9
+ import { channelAfterSessionManager } from '../utils/sessionChannel'
10
+ import {
11
+ buildB2bSession,
12
+ buildMarketingData,
13
+ buildPersonFromProfile,
14
+ buildSessionSearchParams,
15
+ getPreciseLocationData,
16
+ resolveJwtClaims,
17
+ resolveSellerInRegion,
18
+ } from '../utils/validateSessionHelpers'
19
+
20
+ type SessionStoreNamespace = {
21
+ channel?: { value?: string | null } | null
22
+ currencyCode?: { value?: string | null } | null
23
+ currencySymbol?: { value?: string | null } | null
24
+ countryCode?: { value?: string | null } | null
25
+ }
36
26
 
37
- throw err
38
- }
27
+ type SessionCheckoutNamespace = {
28
+ regionId?: { value?: string | null } | null
39
29
  }
40
30
 
41
31
  export const validateSession = async (
@@ -49,7 +39,6 @@ export const validateSession = async (
49
39
  let city = oldSession.city ?? null
50
40
  let geoCoordinates = oldSession.geoCoordinates ?? null
51
41
 
52
- // Update location data if postal code and country are provided
53
42
  const shouldGetPreciseLocation = !city || !geoCoordinates
54
43
  if (shouldGetPreciseLocation && postalCode !== '' && country !== '') {
55
44
  const preciseLocation = await getPreciseLocationData(
@@ -65,64 +54,20 @@ export const validateSession = async (
65
54
  * The Session Manager API (https://developers.vtex.com/docs/api-reference/session-manager-api#patch-/api/sessions) adds the query params to the session public namespace.
66
55
  * This is used by Checkout (checkout-session) and Intelligent Search (search-session)
67
56
  */
68
- const params = new URLSearchParams(search)
69
-
70
- // Remove facets parameter if it exists so that it does not interfere with session data and prioritize vtex_segment
71
- if (params.has('facets')) {
72
- params.delete('facets')
73
- }
74
-
75
- const salesChannel = params.get('sc') ?? channel.salesChannel
76
- params.set('sc', salesChannel)
77
-
78
- if (!!postalCode) {
79
- params.set('postalCode', postalCode)
80
- }
81
-
82
- if (!!country) {
83
- params.set('country', country)
84
- }
85
-
86
- if (!!geoCoordinates) {
87
- params.set(
88
- 'geoCoordinates',
89
- `${geoCoordinates.longitude},${geoCoordinates.latitude}` // long,lat is the format expected
90
- )
91
- }
92
-
93
- // Sending the locale to the session, the store-session app will update cultureInfo
94
- params.set('locale', oldSession.locale)
95
-
96
- const { marketingData: oldMarketingData } = oldSession
97
-
98
- const marketingData: StoreMarketingData = {
99
- utmCampaign:
100
- params.get('utm_campaign') ?? oldMarketingData?.utmCampaign ?? '',
101
- utmMedium: params.get('utm_medium') ?? oldMarketingData?.utmMedium ?? '',
102
- utmSource: params.get('utm_source') ?? oldMarketingData?.utmSource ?? '',
103
- utmiCampaign: params.get('utmi_cp') ?? oldMarketingData?.utmiCampaign ?? '',
104
- utmiPage: params.get('utmi_p') ?? oldMarketingData?.utmiPage ?? '',
105
- utmiPart: params.get('utmi_pc') ?? oldMarketingData?.utmiPart ?? '',
106
- }
107
-
108
- const jwt = parseJwt(getAuthCookie(headers?.cookie ?? '', account))
109
-
110
- // Validate JWT token if it exists
111
- let isValidJwt = false
112
- if (jwt) {
113
- try {
114
- const vtexIdResponse = await clients.commerce.vtexid.validate()
115
- isValidJwt = vtexIdResponse?.authStatus?.toLowerCase() === 'success'
116
- } catch (error) {
117
- console.warn('JWT validation failed:', error)
118
- isValidJwt = false
119
- }
120
- }
121
-
122
- // Only use JWT data if the token is valid
123
- const isRepresentative = isValidJwt ? jwt?.isRepresentative : false
124
- const customerId = isValidJwt ? jwt?.customerId : undefined
125
- const unitId = isValidJwt ? jwt?.unitId : undefined
57
+ const params = buildSessionSearchParams(
58
+ search,
59
+ channel,
60
+ postalCode,
61
+ country,
62
+ geoCoordinates,
63
+ oldSession.locale
64
+ )
65
+ const marketingData = buildMarketingData(params, oldSession.marketingData)
66
+ const { isRepresentative, customerId, unitId } = await resolveJwtClaims(
67
+ clients,
68
+ headers?.cookie,
69
+ account
70
+ )
126
71
 
127
72
  const sessionData = await clients.commerce
128
73
  .session(params.toString())
@@ -130,37 +75,34 @@ export const validateSession = async (
130
75
 
131
76
  const profile = sessionData?.namespaces.profile ?? null
132
77
  const shopper = sessionData?.namespaces.shopper ?? null
133
- const store = sessionData?.namespaces.store ?? null
78
+ const store = (sessionData?.namespaces.store ??
79
+ null) as SessionStoreNamespace | null
134
80
  const authentication = sessionData?.namespaces.authentication ?? null
135
- const checkout = sessionData?.namespaces.checkout ?? null
81
+ const checkout = (sessionData?.namespaces.checkout ??
82
+ null) as SessionCheckoutNamespace | null
136
83
  const publicData = sessionData?.namespaces.public ?? null
137
84
 
138
- // Fetch contract data for B2B representatives
139
85
  let contract = null
140
86
  if (isRepresentative && profile?.id?.value) {
141
87
  try {
142
88
  contract = await clients.commerce.masterData.getContractById({
143
89
  contractId: profile.id.value,
144
90
  })
145
- } catch (err) {
91
+ } catch {
146
92
  console.error(
147
93
  `Error while getting contract data for profile ID (${profile.id.value}).\n`
148
94
  )
149
95
  }
150
96
  }
151
97
 
152
- // Set seller only if it's inside a region
153
- let seller
154
- if (!!channel.seller && (postalCode || geoCoordinates)) {
155
- const regionData = await clients.commerce.checkout.region({
156
- postalCode,
157
- geoCoordinates,
158
- country,
159
- salesChannel,
160
- })
161
- const region = regionData?.[0]
162
- seller = region?.sellers.find((seller) => channel.seller === seller.id)
163
- }
98
+ const sellerId = await resolveSellerInRegion(
99
+ clients,
100
+ channel,
101
+ postalCode,
102
+ geoCoordinates,
103
+ country,
104
+ params.get('sc') ?? channel.salesChannel
105
+ )
164
106
 
165
107
  const newSession = {
166
108
  ...oldSession,
@@ -169,49 +111,30 @@ export const validateSession = async (
169
111
  symbol: store?.currencySymbol?.value ?? oldSession.currency.symbol,
170
112
  },
171
113
  country: store?.countryCode?.value ?? country,
172
- channel: ChannelMarshal.stringify({
173
- salesChannel: store?.channel?.value ?? channel.salesChannel,
174
- regionId: checkout?.regionId?.value ?? channel.regionId,
175
- seller: seller?.id,
176
- hasOnlyDefaultSalesChannel: !store?.channel?.value,
177
- }),
114
+ channel: channelAfterSessionManager(
115
+ channel,
116
+ store?.channel?.value,
117
+ checkout?.regionId?.value,
118
+ sellerId
119
+ ),
178
120
  /**
179
121
  * B2B data structure in Session:
180
122
  * - Logged user data (shopper): `shopper` namespace
181
123
  * - Unit data: `authentication` namespace
182
124
  * - Contract data: `profile` namespace (those info will be available inside Faststore's Session `person` object)
183
125
  */
184
- b2b: isRepresentative
185
- ? {
186
- isRepresentative: isRepresentative ?? false,
187
- customerId: authentication?.customerId?.value ?? customerId ?? '',
188
- unitName: authentication?.unitName?.value ?? '',
189
- unitId: authentication?.unitId?.value ?? unitId ?? '',
190
- firstName:
191
- typeof shopper?.firstName?.value === 'string'
192
- ? shopper.firstName.value
193
- : '',
194
- lastName:
195
- typeof shopper?.lastName?.value === 'string'
196
- ? shopper.lastName.value
197
- : '',
198
- userName:
199
- `${typeof shopper?.firstName?.value === 'string' ? shopper.firstName.value : ''} ${typeof shopper?.lastName?.value === 'string' ? shopper.lastName.value : ''}`.trim(),
200
- userEmail: authentication?.storeUserEmail.value ?? '',
201
- savedPostalCode: publicData?.postalCode?.value ?? '',
202
- contractName: resolveActiveContractDisplayName(contract, profile),
203
- organizationManager: shopper?.organizationManager?.value ?? false,
204
- }
205
- : null,
126
+ b2b: buildB2bSession({
127
+ isRepresentative,
128
+ authentication,
129
+ shopper,
130
+ publicData,
131
+ profile,
132
+ contract,
133
+ customerId,
134
+ unitId,
135
+ }),
206
136
  marketingData,
207
- person: profile?.id
208
- ? {
209
- id: profile.id?.value ?? '',
210
- email: profile.email?.value ?? '',
211
- givenName: profile.firstName?.value ?? '',
212
- familyName: profile.lastName?.value ?? '',
213
- }
214
- : null,
137
+ person: buildPersonFromProfile(profile),
215
138
  geoCoordinates:
216
139
  (geoCoordinates?.latitude &&
217
140
  geoCoordinates?.longitude &&
@@ -14,6 +14,12 @@ type StoreOrder {
14
14
  Indicates whether or not items with attachments should be split.
15
15
  """
16
16
  shouldSplitItem: Boolean
17
+ """
18
+ Sales channel of the underlying orderForm when FastStore adopted it because
19
+ the browser session was stale (e.g. after Quick Order). Clients should align
20
+ `session.channel` to this value. Null when no SC adoption happened.
21
+ """
22
+ salesChannel: String
17
23
  }
18
24
 
19
25
  """
@@ -0,0 +1,31 @@
1
+ import type { Channel } from './channel'
2
+ import ChannelMarshal from './channel'
3
+
4
+ /**
5
+ * When session SC and orderForm SC diverge, keep Checkout on the orderForm
6
+ * trade policy. Used after external cart changes (stale etag / Quick Order) and
7
+ * on later validations while the browser session still lags.
8
+ *
9
+ * Forcing the session SC (refetch / item updates with `sc=session`) would drop
10
+ * items that exist only on the orderForm's sales channel.
11
+ */
12
+ export function channelWhenSessionDivergesFromOrderForm(
13
+ currentChannel: Required<Channel>,
14
+ orderFormSalesChannel: string | null | undefined
15
+ ): string | null {
16
+ if (orderFormSalesChannel == null || orderFormSalesChannel === '') {
17
+ return null
18
+ }
19
+
20
+ if (
21
+ String(orderFormSalesChannel) === String(currentChannel.salesChannel ?? '')
22
+ ) {
23
+ return null
24
+ }
25
+
26
+ return ChannelMarshal.stringify({
27
+ ...currentChannel,
28
+ salesChannel: String(orderFormSalesChannel),
29
+ hasOnlyDefaultSalesChannel: false,
30
+ })
31
+ }
@@ -0,0 +1,32 @@
1
+ import type { Channel } from './channel'
2
+ import ChannelMarshal from './channel'
3
+
4
+ /**
5
+ * Builds the session `channel` string after Session Manager responds.
6
+ *
7
+ * When the client already pinned an explicit SC (e.g. adopted from the
8
+ * orderForm after Quick Order), keep it. Session Manager often still reports
9
+ * the default SC and would otherwise overwrite the adoption.
10
+ */
11
+ export function channelAfterSessionManager(
12
+ channel: Required<Channel>,
13
+ storeChannelValue: string | null | undefined,
14
+ regionId: string | null | undefined,
15
+ sellerId: string | undefined
16
+ ): string {
17
+ const hasExplicitSalesChannel = channel.hasOnlyDefaultSalesChannel === false
18
+
19
+ const storeSalesChannel = storeChannelValue ?? undefined
20
+ const resolvedRegionId = regionId ?? channel.regionId
21
+
22
+ return ChannelMarshal.stringify({
23
+ salesChannel: hasExplicitSalesChannel
24
+ ? channel.salesChannel || storeSalesChannel
25
+ : (storeSalesChannel ?? channel.salesChannel),
26
+ regionId: resolvedRegionId ?? undefined,
27
+ seller: sellerId,
28
+ hasOnlyDefaultSalesChannel: hasExplicitSalesChannel
29
+ ? false
30
+ : !storeSalesChannel,
31
+ })
32
+ }
@@ -0,0 +1,202 @@
1
+ import type { GraphqlContext } from '..'
2
+ import type {
3
+ StoreMarketingData,
4
+ StoreSession,
5
+ } from '../../../__generated__/schema'
6
+ import type { Channel } from './channel'
7
+ import { resolveActiveContractDisplayName } from './contract'
8
+ import { getAuthCookie, parseJwt } from './cookies'
9
+
10
+ export async function getPreciseLocationData(
11
+ clients: GraphqlContext['clients'],
12
+ country: string,
13
+ postalCode: string
14
+ ) {
15
+ try {
16
+ const address = await clients.commerce.checkout.address({
17
+ postalCode,
18
+ country,
19
+ })
20
+
21
+ const geoCoordinates = address.geoCoordinates
22
+ ? {
23
+ latitude: address.geoCoordinates[1],
24
+ longitude: address.geoCoordinates[0],
25
+ }
26
+ : null
27
+
28
+ return { city: address.city, geoCoordinates }
29
+ } catch (err) {
30
+ console.error(
31
+ `Error while getting geo coordinates for the current postal code (${postalCode}) and country (${country}).\n`
32
+ )
33
+
34
+ throw err
35
+ }
36
+ }
37
+
38
+ export function buildSessionSearchParams(
39
+ search: string,
40
+ channel: Required<Channel>,
41
+ postalCode: string,
42
+ country: string,
43
+ geoCoordinates: StoreSession['geoCoordinates'],
44
+ locale: string
45
+ ) {
46
+ const params = new URLSearchParams(search)
47
+
48
+ // Remove facets so they do not interfere with session data / vtex_segment
49
+ if (params.has('facets')) {
50
+ params.delete('facets')
51
+ }
52
+
53
+ params.set('sc', params.get('sc') ?? channel.salesChannel)
54
+
55
+ if (postalCode) {
56
+ params.set('postalCode', postalCode)
57
+ }
58
+
59
+ if (country) {
60
+ params.set('country', country)
61
+ }
62
+
63
+ if (geoCoordinates) {
64
+ params.set(
65
+ 'geoCoordinates',
66
+ `${geoCoordinates.longitude},${geoCoordinates.latitude}` // long,lat is the format expected
67
+ )
68
+ }
69
+
70
+ // Sending the locale to the session, the store-session app will update cultureInfo
71
+ params.set('locale', locale)
72
+
73
+ return params
74
+ }
75
+
76
+ export function buildMarketingData(
77
+ params: URLSearchParams,
78
+ oldMarketingData: StoreSession['marketingData']
79
+ ): StoreMarketingData {
80
+ return {
81
+ utmCampaign:
82
+ params.get('utm_campaign') ?? oldMarketingData?.utmCampaign ?? '',
83
+ utmMedium: params.get('utm_medium') ?? oldMarketingData?.utmMedium ?? '',
84
+ utmSource: params.get('utm_source') ?? oldMarketingData?.utmSource ?? '',
85
+ utmiCampaign: params.get('utmi_cp') ?? oldMarketingData?.utmiCampaign ?? '',
86
+ utmiPage: params.get('utmi_p') ?? oldMarketingData?.utmiPage ?? '',
87
+ utmiPart: params.get('utmi_pc') ?? oldMarketingData?.utmiPart ?? '',
88
+ }
89
+ }
90
+
91
+ export async function resolveJwtClaims(
92
+ clients: GraphqlContext['clients'],
93
+ cookie: string | undefined,
94
+ account: string
95
+ ) {
96
+ const jwt = parseJwt(getAuthCookie(cookie ?? '', account))
97
+ let isValidJwt = false
98
+
99
+ if (jwt) {
100
+ try {
101
+ const vtexIdResponse = await clients.commerce.vtexid.validate()
102
+ isValidJwt = vtexIdResponse?.authStatus?.toLowerCase() === 'success'
103
+ } catch (error) {
104
+ console.warn('JWT validation failed:', error)
105
+ isValidJwt = false
106
+ }
107
+ }
108
+
109
+ return {
110
+ isRepresentative: isValidJwt ? Boolean(jwt?.isRepresentative) : false,
111
+ customerId: isValidJwt ? jwt?.customerId : undefined,
112
+ unitId: isValidJwt ? jwt?.unitId : undefined,
113
+ }
114
+ }
115
+
116
+ export async function resolveSellerInRegion(
117
+ clients: GraphqlContext['clients'],
118
+ channel: Required<Channel>,
119
+ postalCode: string,
120
+ geoCoordinates: StoreSession['geoCoordinates'],
121
+ country: string,
122
+ salesChannel: string
123
+ ) {
124
+ if (!channel.seller || !(postalCode || geoCoordinates)) {
125
+ return undefined
126
+ }
127
+
128
+ const regionData = await clients.commerce.checkout.region({
129
+ postalCode,
130
+ geoCoordinates,
131
+ country,
132
+ salesChannel,
133
+ })
134
+ const region = regionData?.[0]
135
+
136
+ return region?.sellers.find((seller) => channel.seller === seller.id)?.id
137
+ }
138
+
139
+ function shopperString(value: { value?: unknown } | null | undefined): string {
140
+ return typeof value?.value === 'string' ? value.value : ''
141
+ }
142
+
143
+ export function buildB2bSession(args: {
144
+ isRepresentative: boolean
145
+ authentication: {
146
+ customerId?: { value?: string | null } | null
147
+ unitName?: { value?: string | null } | null
148
+ unitId?: { value?: string | null } | null
149
+ storeUserEmail?: { value?: string | null } | null
150
+ } | null
151
+ shopper: {
152
+ firstName?: { value?: unknown } | null
153
+ lastName?: { value?: unknown } | null
154
+ organizationManager?: { value?: boolean | null } | null
155
+ } | null
156
+ publicData: { postalCode?: { value?: string | null } | null } | null
157
+ profile: Parameters<typeof resolveActiveContractDisplayName>[1]
158
+ contract: Parameters<typeof resolveActiveContractDisplayName>[0]
159
+ customerId?: string
160
+ unitId?: string
161
+ }) {
162
+ if (!args.isRepresentative) {
163
+ return null
164
+ }
165
+
166
+ const firstName = shopperString(args.shopper?.firstName)
167
+ const lastName = shopperString(args.shopper?.lastName)
168
+
169
+ return {
170
+ isRepresentative: true,
171
+ customerId: args.authentication?.customerId?.value ?? args.customerId ?? '',
172
+ unitName: args.authentication?.unitName?.value ?? '',
173
+ unitId: args.authentication?.unitId?.value ?? args.unitId ?? '',
174
+ firstName,
175
+ lastName,
176
+ userName: `${firstName} ${lastName}`.trim(),
177
+ userEmail: args.authentication?.storeUserEmail?.value ?? '',
178
+ savedPostalCode: args.publicData?.postalCode?.value ?? '',
179
+ contractName: resolveActiveContractDisplayName(args.contract, args.profile),
180
+ organizationManager: args.shopper?.organizationManager?.value ?? false,
181
+ }
182
+ }
183
+
184
+ export function buildPersonFromProfile(
185
+ profile: {
186
+ id?: { value?: string | null } | null
187
+ email?: { value?: string | null } | null
188
+ firstName?: { value?: string | null } | null
189
+ lastName?: { value?: string | null } | null
190
+ } | null
191
+ ) {
192
+ if (!profile?.id) {
193
+ return null
194
+ }
195
+
196
+ return {
197
+ id: profile.id?.value ?? '',
198
+ email: profile.email?.value ?? '',
199
+ givenName: profile.firstName?.value ?? '',
200
+ familyName: profile.lastName?.value ?? '',
201
+ }
202
+ }