Skip to content

feat: Delivery Promise - navigation based on shopper location #2716

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/api/src/__generated__/schema.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions packages/api/src/platforms/vtex/clients/commerce/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,17 +357,19 @@ export const VtexCommerce = (

params.set(
'items',
'profile.id,profile.email,profile.firstName,profile.lastName,store.channel,store.countryCode,store.cultureInfo,store.currencyCode,store.currencySymbol,authentication.customerId,'
'profile.id,profile.email,profile.firstName,profile.lastName,store.channel,store.countryCode,store.cultureInfo,store.currencyCode,store.currencySymbol,authentication.customerId,checkout.regionId,'
)

const headers: HeadersInit = withCookie({
'content-type': 'application/json',
})

const sessionCookie = parse(ctx?.headers?.cookie ?? '')?.vtex_session

return fetchAPI(
`${base}/api/sessions?${params.toString()}`,
{
method: 'POST',
method: sessionCookie ? 'PATCH' : 'POST',
headers,
body: '{}',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ export interface Address {
neighborhood: string
complement: string
reference: string
geoCoordinates: [number]
geoCoordinates: [number, number] // [longitude, latitude]
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface Profile {

export interface Checkout {
orderFormId?: Value
regionId?: Value
}

export interface Public {
Expand Down
17 changes: 17 additions & 0 deletions packages/api/src/platforms/vtex/clients/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
ProductSearchResult,
Suggestion,
} from './types/ProductSearchResult'
import type { ProductCountResult } from './types/ProductCountResult'

export type Sort =
| 'price:desc'
Expand Down Expand Up @@ -265,10 +266,26 @@ export const IntelligentSearch = (
const facets = (args: Omit<SearchArgs, 'type'>) =>
search<FacetSearchResult>({ ...args, type: 'facets' })

const productCount = (
args: Pick<SearchArgs, 'query'>
): Promise<ProductCountResult> => {
const params = new URLSearchParams()

if (args?.query) {
params.append('query', args.query.toString())
}

return fetchAPI(
`${base}/_v/api/intelligent-search/catalog_count?${params.toString()}`,
{ headers }
)
}

return {
facets,
products,
suggestedTerms,
topSearches,
productCount,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface ProductCountResult {
/**
* @description Total product count.
*/
total: number
}
16 changes: 16 additions & 0 deletions packages/api/src/platforms/vtex/resolvers/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
QuerySearchArgs,
QuerySellersArgs,
QueryShippingArgs,
QueryProductCountArgs,
} from '../../../__generated__/schema'
import { BadRequestError, NotFoundError } from '../../errors'
import type { CategoryTree } from '../clients/commerce/types/CategoryTree'
Expand Down Expand Up @@ -361,4 +362,19 @@ export const Query = {

return { addresses: parsedAddresses }
},
productCount: async (
_: unknown,
{ term }: QueryProductCountArgs,
ctx: Context
) => {
const {
clients: { search },
} = ctx

const result = await search.productCount({
query: term ?? undefined,
})

return result
},
}
93 changes: 75 additions & 18 deletions packages/api/src/platforms/vtex/resolvers/validateSession.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,34 @@
import deepEquals from 'fast-deep-equal'

import ChannelMarshal from '../utils/channel'
import type { Context } from '..'
import type {
MutationValidateSessionArgs,
StoreMarketingData,
StoreSession,
} from '../../../__generated__/schema'
import ChannelMarshal from '../utils/channel'

async function getPreciseLocationData(
clients: Context['clients'],
country: string,
postalCode: string
) {
try {
const address = await clients.commerce.checkout.address({
postalCode,
country,
})

const [longitude, latitude] = address.geoCoordinates
return { city: address.city, geoCoordinates: { latitude, longitude } }
} catch (err) {
console.error(
`Error while getting geo coordinates for the current postal code (${postalCode}) and country (${country}).\n`
)

throw err
}
}

export const validateSession = async (
_: any,
Expand All @@ -15,15 +37,45 @@ export const validateSession = async (
): Promise<StoreSession | null> => {
const channel = ChannelMarshal.parse(oldSession.channel ?? '')
const postalCode = String(oldSession.postalCode ?? '')
const geoCoordinates = oldSession.geoCoordinates ?? null

const country = oldSession.country ?? ''
let city = oldSession.city ?? null
let geoCoordinates = oldSession.geoCoordinates ?? null

// Update location data if postal code and country are provided
const shouldGetPreciseLocation = !city || !geoCoordinates
if (shouldGetPreciseLocation && postalCode !== '' && country !== '') {
const preciseLocation = await getPreciseLocationData(
clients,
country,
postalCode
)
city = preciseLocation.city
geoCoordinates = preciseLocation.geoCoordinates
}

/**
* 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.
* This is used by Checkout (checkout-session) and Intelligent Search (search-session)
*/
const params = new URLSearchParams(search)
const salesChannel = params.get('sc') ?? channel.salesChannel

params.set('sc', salesChannel)

if (!!postalCode) {
params.set('postalCode', postalCode)
}

if (!!country) {
params.set('country', country)
}

if (!!geoCoordinates) {
params.set(
'geoCoordinates',
`${geoCoordinates.latitude},${geoCoordinates.longitude}`
)
}

const { marketingData: oldMarketingData } = oldSession

const marketingData: StoreMarketingData = {
Expand All @@ -36,24 +88,27 @@ export const validateSession = async (
utmiPart: params.get('utmi_pc') ?? oldMarketingData?.utmiPart ?? '',
}

const [regionData, sessionData] = await Promise.all([
postalCode || geoCoordinates
? clients.commerce.checkout.region({
postalCode,
geoCoordinates,
country,
salesChannel,
})
: Promise.resolve(null),
clients.commerce.session(params.toString()).catch(() => null),
])
const sessionData = await clients.commerce
.session(params.toString())
.catch(() => null)

const profile = sessionData?.namespaces.profile ?? null
const store = sessionData?.namespaces.store ?? null
const authentication = sessionData?.namespaces.authentication ?? null
const region = regionData?.[0]
const checkout = sessionData?.namespaces.checkout ?? null

// Set seller only if it's inside a region
const seller = region?.sellers.find((seller) => channel.seller === seller.id)
let seller
if (!!channel.seller && (postalCode || geoCoordinates)) {
const regionData = await clients.commerce.checkout.region({
postalCode,
geoCoordinates,
country,
salesChannel,
})
const region = regionData?.[0]
seller = region?.sellers.find((seller) => channel.seller === seller.id)
}

const newSession = {
...oldSession,
Expand All @@ -64,7 +119,7 @@ export const validateSession = async (
country: store?.countryCode?.value ?? oldSession.country,
channel: ChannelMarshal.stringify({
salesChannel: store?.channel?.value ?? channel.salesChannel,
regionId: region?.id ?? channel.regionId,
regionId: checkout?.regionId?.value ?? channel.regionId,
seller: seller?.id,
hasOnlyDefaultSalesChannel: !store?.channel?.value,
}),
Expand All @@ -80,6 +135,8 @@ export const validateSession = async (
familyName: profile.lastName?.value ?? '',
}
: null,
geoCoordinates,
city,
}

if (deepEquals(oldSession, newSession)) {
Expand Down
18 changes: 18 additions & 0 deletions packages/api/src/typeDefs/query.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ input IGeoCoordinates {
longitude: Float!
}

type ProductCountResult {
"""
Total product count.
"""
total: Int!
}

type Query {
"""
Returns the details of a product based on the specified locator.
Expand Down Expand Up @@ -349,6 +356,17 @@ type Query {
id: String!
): Profile
@cacheControl(scope: "public", sMaxAge: 120, staleWhileRevalidate: 3600)

"""
Returns the total product count information based on a specific location accessible through the VTEX segment cookie.
"""
productCount(
"""
Search term.
"""
term: String
): ProductCountResult
@cacheControl(scope: "public", sMaxAge: 120, staleWhileRevalidate: 3600)
}

"""
Expand Down
8 changes: 8 additions & 0 deletions packages/api/src/typeDefs/session.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ type StoreSession {
"""
addressType: String
"""
Session city.
"""
city: String
"""
Session postal code.
"""
postalCode: String
Expand Down Expand Up @@ -217,6 +221,10 @@ input IStoreSession {
"""
addressType: String
"""
Session input city.
"""
city: String
"""
Session input postal code.
"""
postalCode: String
Expand Down
1 change: 1 addition & 0 deletions packages/api/test/integration/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const QUERIES = [
'redirect',
'sellers',
'profile',
'productCount',
]

const MUTATIONS = ['validateCart', 'validateSession', 'subscribeToNewsletter']
Expand Down
Loading
Loading