Skip to content
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

feat: Support for cross selling API #1396

Merged
merged 8 commits into from
Jul 5, 2022
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
6 changes: 5 additions & 1 deletion packages/api/src/__generated__/schema.ts

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

26 changes: 24 additions & 2 deletions packages/api/src/platforms/vtex/clients/commerce/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { fetchAPI } from '../fetch'
import { FACET_CROSS_SELLING_MAP } from "../../utils/facets"
import { fetchAPI } from "../fetch"
import type { PortalProduct } from "./types/Product"
import type { Context, Options } from '../../index'
import type { Brand } from './types/Brand'
import type { CategoryTree } from './types/CategoryTree'
Expand All @@ -13,6 +15,8 @@ import type {
import type { Session } from './types/Session'
import type { Channel } from '../../utils/channel'

type ValueOf<T> = T extends Record<string, infer K> ? K : never;

const BASE_INIT = {
method: 'POST',
headers: {
Expand Down Expand Up @@ -40,6 +44,24 @@ export const VtexCommerce = (
pagetype: (slug: string): Promise<PortalPagetype> =>
fetchAPI(`${base}/api/catalog_system/pub/portal/pagetype/${slug}`),
},
products: {
crossselling: (
{ type, productId, groupByProduct = true }: {
type: ValueOf<typeof FACET_CROSS_SELLING_MAP>;
productId: string;
groupByProduct?: boolean;
},
): Promise<PortalProduct[]> => {
const params = new URLSearchParams({
sc: ctx.storage.channel.salesChannel,
groupByProduct: groupByProduct.toString(),
icazevedo marked this conversation as resolved.
Show resolved Hide resolved
})

return fetchAPI(
`${base}/api/catalog_system/pub/products/crossselling/${type}/${productId}?${params}`,
)
},
},
},
checkout: {
simulation: (
Expand Down Expand Up @@ -120,7 +142,7 @@ export const VtexCommerce = (
...BASE_INIT,
body: JSON.stringify({ value }),
method: 'PUT',
}
},
)
},
region: async ({
Expand Down
199 changes: 199 additions & 0 deletions packages/api/src/platforms/vtex/clients/commerce/types/Product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
export interface PortalProduct {
productId: string;
productName: string;
brand: string;
brandId: number;
brandImageUrl: null | string;
linkText: string;
productReference: string;
productReferenceCode: string;
categoryId: string;
productTitle: string;
metaTagDescription: string;
releaseDate: Date;
clusterHighlights: unknown;
productClusters: unknown;
searchableClusters: unknown;
categories: Category[];
categoriesIds: CategoriesId[];
link: string;
description: string;
items: Item[];
}

enum Category {
Office = "/Office/",
OfficeChairs = "/Office/Chairs/",
}

enum CategoriesId {
The9282 = "/9282/",
The92829296 = "/9282/9296/",
}



interface Item {
itemId: string;
name: string;
nameComplete: string;
complementName: string;
ean: string;
referenceId: ReferenceId[];
measurementUnit: MeasurementUnit;
unitMultiplier: number;
modalType: null;
isKit: boolean;
images: Image[];
sellers: Seller[];
videos: unknown[];
estimatedDateArrival: null;
}

interface Image {
imageId: string;
imageLabel: string;
imageTag: string;
imageUrl: string;
imageText: string;
imageLastModified: Date;
}

enum MeasurementUnit {
Un = "un",
}

interface ReferenceId {
key: Key;
value: string;
}

enum Key {
RefId = "RefId",
}

interface Seller {
sellerId: string;
sellerName: SellerName;
addToCartLink: string;
sellerDefault: boolean;
commertialOffer: CommertialOffer;
}

interface CommertialOffer {
deliverySlaSamplesPerRegion: DeliverySlaSamplesPerRegion;
installments: Installment[];
discountHighLight: unknown[];
giftSkuIds: unknown[];
teasers: unknown[];
buyTogether: unknown[];
itemMetadataAttachment: unknown[];
price: number;
listPrice: number;
priceWithoutDiscount: number;
rewardValue: number;
priceValidUntil: Date;
availableQuantity: number;
isAvailable: boolean;
tax: number;
deliverySlaSamples: DeliverySlaSample[];
getInfoErrorMessage: null;
cacheVersionUsedToCallCheckout: string;
paymentOptions: PaymentOptions;
}

interface DeliverySlaSample {
deliverySlaPerTypes: unknown[];
region: null;
}

interface DeliverySlaSamplesPerRegion {
the0: DeliverySlaSample;
}

interface Installment {
value: number;
interestRate: number;
totalValuePlusInterestRate: number;
numberOfInstallments: number;
paymentSystemName: PaymentSystemNameEnum;
paymentSystemGroupName: GroupName;
name: Name;
}

enum Name {
BoletoBancárioÀVista = "Boleto Bancário à vista",
FreeÀVista = "Free à vista",
}

enum GroupName {
BankInvoicePaymentGroup = "bankInvoicePaymentGroup",
Custom201PaymentGroupPaymentGroup = "custom201PaymentGroupPaymentGroup",
}

enum PaymentSystemNameEnum {
BoletoBancário = "Boleto Bancário",
Free = "Free",
}

interface PaymentOptions {
installmentOptions: InstallmentOption[];
paymentSystems: PaymentSystem[];
payments: unknown[];
giftCards: unknown[];
giftCardMessages: unknown[];
availableAccounts: unknown[];
availableTokens: unknown[];
}

interface InstallmentOption {
paymentSystem: string;
bin: null;
paymentName: PaymentSystemNameEnum;
paymentGroupName: GroupName;
value: number;
installments: InstallmentElement[];
}

interface InstallmentElement {
count: number;
hasInterestRate: boolean;
interestRate: number;
value: number;
total: number;
sellerMerchantInstallments?: InstallmentElement[];
id?: Id;
}

enum Id {
Storeframework = "STOREFRAMEWORK",
}

interface PaymentSystem {
id: number;
name: PaymentSystemNameEnum;
groupName: GroupName;
validator: null;
stringId: string;
template: Template;
requiresDocument: boolean;
isCustom: boolean;
description: Description | null;
requiresAuthentication: boolean;
dueDate: Date;
availablePayments: null;
}

enum Description {
FreePayToTestCheckoutPayments = "Free pay to test checkout payments",
}

enum Template {
BankInvoicePaymentGroupTemplate = "bankInvoicePaymentGroup-template",
Custom201PaymentGroupPaymentGroupTemplate =
"custom201PaymentGroupPaymentGroup-template",
}

enum SellerName {
Vtex = "VTEX",
}
53 changes: 39 additions & 14 deletions packages/api/src/platforms/vtex/resolvers/query.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import { NotFoundError, BadRequestError } from '../../errors'
import { mutateChannelContext, mutateLocaleContext } from '../utils/contex'
import { enhanceSku } from '../utils/enhanceSku'
import { FACET_CROSS_SELLING_MAP } from "./../utils/facets"
import { BadRequestError, NotFoundError } from "../../errors"
import { mutateChannelContext, mutateLocaleContext } from "../utils/contex"
import { enhanceSku } from "../utils/enhanceSku"
import {
findChannel,
findCrossSelling,
findLocale,
findSkuId,
findSlug,
transformSelectedFacet,
} from '../utils/facets'
import { SORT_MAP } from '../utils/sort'
import { StoreCollection } from './collection'
} from "../utils/facets"
import { SORT_MAP } from "../utils/sort"
import { StoreCollection } from "./collection"
import type {
QueryProductArgs,
QueryAllCollectionsArgs,
QueryAllProductsArgs,
QuerySearchArgs,
QueryCollectionArgs,
} from '../../../__generated__/schema'
import type { CategoryTree } from '../clients/commerce/types/CategoryTree'
import type { Context } from '../index'
import { isValidSkuId, pickBestSku } from '../utils/sku'
QueryProductArgs,
QuerySearchArgs,
} from "../../../__generated__/schema"
import type { CategoryTree } from "../clients/commerce/types/CategoryTree"
import type { Context } from "../index"
import { isValidSkuId, pickBestSku } from "../utils/sku"

export const Query = {
product: async (_: unknown, { locator }: QueryProductArgs, ctx: Context) => {
Expand Down Expand Up @@ -95,6 +97,7 @@ export const Query = {
// Insert channel in context for later usage
const channel = findChannel(selectedFacets)
const locale = findLocale(selectedFacets)
const crossSelling = findCrossSelling(selectedFacets)

if (channel) {
mutateChannelContext(ctx, channel)
Expand All @@ -104,11 +107,33 @@ export const Query = {
mutateLocaleContext(ctx, locale)
}

let query = term

/**
* In case we are using crossSelling, we need to modify the search
* we will be performing on our search engine. The idea in here
* is to use the cross selling API for fetching the productIds our
* search will return for us.
* Doing this two request workflow makes it possible to have cross
* selling with Search features, like pagination, internationalization
* etc
*/
if (crossSelling) {
const products = await ctx.clients.commerce.catalog.products.crossselling({
type: FACET_CROSS_SELLING_MAP[crossSelling.key],
productId: crossSelling.value,
})

query = `product:${
products.map((x) => x.productId).slice(0, first).join(";")
}`
}

const after = maybeAfter ? Number(maybeAfter) : 0
const searchArgs = {
page: Math.ceil(after / first!),
page: Math.ceil(after / first),
count: first,
query: term,
query,
sort: SORT_MAP[sort ?? 'score_desc'],
selectedFacets: selectedFacets?.flatMap(transformSelectedFacet) ?? [],
}
Expand Down
Loading