Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6a13c8c
chore: lern force-publish release
lemagnetic Jun 1, 2026
e9b2105
fix: fix vtex message
lemagnetic Jun 1, 2026
516e784
[no ci] Release: 4.2.1
lemagnetic Jun 1, 2026
da907bc
chore: revert accidental 4.2.1 release (#3375) [skip ci]
lemagnetic Jun 1, 2026
a0c982a
feat: add B2B contract switcher to the FastStore account drawer
rodrigo-tavares Jun 8, 2026
2fa360f
feat(core): wire contract switcher to real SDK hooks and remove UI mocks
rodrigo-tavares Jun 12, 2026
54bd372
fix: address contract switcher PR review feedback
rodrigo-tavares Jun 23, 2026
ba36ccd
chore: merge dev and add contract switcher SDD spec
rodrigo-tavares Jun 23, 2026
18dfa15
fix(core): resolve Sonar issues in contract switcher
rodrigo-tavares Jun 24, 2026
1dbcbdd
refactor(api): load availableContracts via buyer-portal store-front BFF
rodrigo-tavares Jun 25, 2026
04c5060
feat(core): wire contract switch to session and switch-properties
rodrigo-tavares Jun 25, 2026
8c75f56
chore: merge dev and resolve contract switcher conflicts
rodrigo-tavares Jun 25, 2026
95c634d
fix: correct availableContracts cacheControl and local-only dev config
rodrigo-tavares Jun 26, 2026
c3b4a20
style(api): format availableContracts cacheControl directive
rodrigo-tavares Jun 26, 2026
beb90a7
fix(core): add switchError to ContractSwitcherContentProps
rodrigo-tavares Jun 26, 2026
17aaa9d
fix(core): resolve Sonar issues and raise new-code test coverage
rodrigo-tavares Jun 26, 2026
d9f0430
style(core): compare globalThis.window with undefined directly
rodrigo-tavares Jun 26, 2026
4a9c204
test: raise contract switcher new-code coverage for Sonar gate
rodrigo-tavares Jun 26, 2026
bc6d1f5
fix(core): apply auth cookie from contract switch response
rodrigo-tavares Jun 26, 2026
6072feb
fix(core): address CodeRabbit review for contract switcher
rodrigo-tavares Jun 26, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,7 @@ packages/core/@generated/cached-operations.json
/tmp
oclif.manifest.json

# Local store overrides (copy from discovery.config.local.example.js)
packages/core/discovery.config.local.js

*storybook.log
22 changes: 22 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.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface ContractResponse {
isCorporate: boolean
corporateName: string
email?: string
firstName: string
lastName: string
userName: string
Expand Down
17 changes: 14 additions & 3 deletions packages/api/src/platforms/vtex/clients/commerce/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ export const VtexCommerce = (

const forwardedHost = host.replace(selectedPrefix, '')

const withBuyerAuthHeaders = (
additionalHeaders: Record<string, string> = {}
): HeadersInit => {
const authToken = getAuthCookie(getUpdatedCookie(ctx) ?? '', account)

return withCookie({
...additionalHeaders,
'X-FORWARDED-HOST': forwardedHost,
...(authToken ? { [`VtexIdclientAutCookie_${account}`]: authToken } : {}),
})
}

return {
catalog: {
salesChannel: (sc: string): Promise<SalesChannel> =>
Expand Down Expand Up @@ -453,7 +465,7 @@ export const VtexCommerce = (

params.set(
'items',
'profile.id,profile.email,profile.firstName,profile.lastName,profile.phone,shopper.firstName,shopper.lastName,shopper.organizationManager,store.channel,store.countryCode,store.cultureInfo,store.currencyCode,store.currencySymbol,authentication.customerId,authentication.storeUserId,authentication.storeUserEmail,authentication.unitId,authentication.unitName,checkout.regionId,public.postalCode'
'profile.id,profile.email,profile.firstName,profile.lastName,profile.phone,shopper.firstName,shopper.lastName,shopper.organizationManager,shopper.availableContracts,shopper.activeContractId,store.channel,store.countryCode,store.cultureInfo,store.currencyCode,store.currencySymbol,authentication.customerId,authentication.storeUserId,authentication.storeUserEmail,authentication.unitId,authentication.unitName,checkout.regionId,public.postalCode'
)

const headers: HeadersInit = withCookie({
Expand Down Expand Up @@ -760,10 +772,9 @@ export const VtexCommerce = (
throw new BadRequestError('Missing contractId to fetch CL fields.')
}

const headers: HeadersInit = withAppKeyAndToken({
const headers: HeadersInit = withBuyerAuthHeaders({
Accept: 'application/json',
'content-type': 'application/json',
'X-FORWARDED-HOST': forwardedHost,
})

return fetchAPI(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,19 @@ export interface Profile {
phone?: Value
}

export type SessionAvailableContract = {
customerId: string
contractName: string
isActive: boolean
isCurrent: boolean
}

export interface Shopper {
firstName?: Value
lastName?: Value
organizationManager?: { value: boolean }
availableContracts?: { value: SessionAvailableContract[] }
activeContractId?: Value
}

export interface Checkout {
Expand Down
61 changes: 58 additions & 3 deletions packages/api/src/platforms/vtex/resolvers/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
ProcessOrderAuthorizationRule,
QueryAllCollectionsArgs,
QueryAllProductsArgs,
QueryAvailableContractsArgs,
QueryCollectionArgs,
QueryListUserOrdersArgs,
QueryPickupPointsArgs,
Expand All @@ -14,6 +15,7 @@ import type {
QuerySellersArgs,
QueryShippingArgs,
QueryUserOrderArgs,
StoreContract,
UserOrderFromList,
} from '../../../__generated__/schema'
import { getOrderEntryOperation } from './getOrderEntryOperation'
Expand All @@ -31,6 +33,12 @@ import type { SearchArgs } from '../clients/search'
import type { ProductSearchResult } from '../clients/search/types/ProductSearchResult'
import type { GraphqlContext } from '../index'
import { extractRuleForAuthorization } from '../utils/commercialAuth'
import {
mapSessionContractsToStoreContracts,
parseSessionAvailableContracts,
resolveActiveContractDisplayName,
resolveActiveContractIdFromSession,
} from '../utils/contract'
import { mutateChannelContext, mutateLocaleContext } from '../utils/contex'
import { getAuthCookie, parseJwt } from '../utils/cookies'
import { enhanceSku } from '../utils/enhanceSku'
Expand Down Expand Up @@ -664,9 +672,7 @@ export const Query = {
contractId: profile?.id?.value ?? '',
})

const name =
contract?.corporateName ??
`${(profile?.firstName?.value ?? '').trim()} ${(profile?.lastName?.value ?? '').trim()}`.trim()
const name = resolveActiveContractDisplayName(contract, profile)

return {
name: name || '',
Expand All @@ -689,6 +695,55 @@ export const Query = {
// createdAt: '',
}
},
// only b2b users
// Contract list from VTEX session `shopper.availableContracts`.
availableContracts: async (
_: unknown,
{ orgUnitId }: QueryAvailableContractsArgs,
ctx: GraphqlContext
): Promise<StoreContract[]> => {
if (!orgUnitId) {
throw new BadRequestError('Missing orgUnitId')
}

const {
account,
headers,
clients: { commerce },
} = ctx

const sessionData = await commerce.session('').catch(() => null)
const authToken = getAuthCookie(headers?.cookie ?? '', account)
let jwt: ReturnType<typeof parseJwt> = null

try {
jwt = authToken ? parseJwt(authToken) : null
} catch {
jwt = null
}

const sessionUnitId =
sessionData?.namespaces.authentication?.unitId?.value?.trim() ??
jwt?.unitId?.trim() ??
''

if (!sessionUnitId || sessionUnitId !== orgUnitId) {
throw new ForbiddenError(
'You are not allowed to list contracts for this organization unit'
)
}

const contracts = parseSessionAvailableContracts(
sessionData?.namespaces.shopper
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const activeContractId =
resolveActiveContractIdFromSession(sessionData) ||
jwt?.customerId?.trim() ||
''

return mapSessionContractsToStoreContracts(contracts, activeContractId)
},
pickupPoints: async (
_: unknown,
{ geoCoordinates }: QueryPickupPointsArgs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
StoreSession,
} from '../../../__generated__/schema'
import ChannelMarshal from '../utils/channel'
import { resolveActiveContractDisplayName } from '../utils/contract'
import { getAuthCookie, parseJwt } from '../utils/cookies'

async function getPreciseLocationData(
Expand Down Expand Up @@ -198,7 +199,7 @@ export const validateSession = async (
`${typeof shopper?.firstName?.value === 'string' ? shopper.firstName.value : ''} ${typeof shopper?.lastName?.value === 'string' ? shopper.lastName.value : ''}`.trim(),
userEmail: authentication?.storeUserEmail.value ?? '',
savedPostalCode: publicData?.postalCode?.value ?? '',
contractName: contract?.corporateName ?? '',
contractName: resolveActiveContractDisplayName(contract, profile),
organizationManager: shopper?.organizationManager?.value ?? false,
}
: null,
Expand Down
18 changes: 18 additions & 0 deletions packages/api/src/platforms/vtex/typeDefs/organization.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,21 @@ input IStoreOrganization {
"""
identifier: String!
}

"""
A commercial contract available to a buyer's Organization Unit.
"""
type StoreContract {
"""
Contract identifier (the contract/scope ID associated with the Organization Unit).
"""
id: ID!
"""
Human-readable corporate name of the contract (resolved from MasterData).
"""
corporateName: String!
"""
Indicates whether this contract is the one currently active in the session.
"""
isActive: Boolean!
}
12 changes: 12 additions & 0 deletions packages/api/src/platforms/vtex/typeDefs/query.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,18 @@ type Query {
@auth
@cacheControl(scope: "private", sMaxAge: 300, staleWhileRevalidate: 3600)

"""
Lists the commercial contracts associated with the given Organization Unit,
resolved to human-readable corporate names. Governed: only contracts associated
with the authenticated buyer's Organization Unit are returned.
"""
availableContracts(
"""
The Organization Unit identifier whose contracts will be listed.
"""
orgUnitId: String!
): [StoreContract!]! @auth @cacheControl(scope: "private", sMaxAge: 0)

"""
Returns a list of pickup points near to the given geo coordinates.
"""
Expand Down
Loading
Loading