Skip to content

Commit 9cf06ef

Browse files
Guileraartursantiago
authored andcommitted
feat: increments commerce client (#2647)
## What's the purpose of this pull request? To add the Reviews & Ratings API integration through commerce client ## How it works? Adds 3 new calls to the client: - client.commerce.rating (retrieves rating information for a specific product) - client.commerce.reviews.list (retrieves all reviews for a specific product) - client.commerce.reviews.create (creates a new review for a specific product) ## How to test it? Creates a `.ts` file on the root folder of the project and adds the following code: ```typescript import { getContextFactory, Options } from "./packages/api/src/platforms/vtex"; const apiOptions = { platform: 'vtex', account: 'storeframework', locale: 'en-US', environment: 'vtexcommercestable', channel: '{"salesChannel":"1"}', showSponsored: false, } as Options const apiCtx = getContextFactory(apiOptions) const commerceApiClient = apiCtx({}).clients.commerce ``` After that you can use the `commerceApiClient` to call the new methods. To run the file locally use the following command: ```bash npx tsx ``` ## References [JIRA Task: SFS-2092](https://vtex-dev.atlassian.net/browse/SFS-2092) [Reviews & Ratings API Doc](https://developers.vtex.com/docs/api-reference/reviews-and-ratings-api#overview) ## Checklist **PR Description** - [ ] Added Rating types - [ ] Added Reviews types - [ ] Incremented ProductSearchReviewResult - [ ] Created adapatObject function on `utils` - [ ] Created camelToSnakeCase function on `utils`
1 parent e64bc11 commit 9cf06ef

File tree

6 files changed

+159
-0
lines changed

6 files changed

+159
-0
lines changed

packages/api/src/platforms/vtex/clients/commerce/index.ts

+53
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ import type { MasterDataResponse } from './types/Newsletter'
2020
import type { Address, AddressInput } from './types/Address'
2121
import type { DeliveryMode, SelectedAddress } from './types/ShippingData'
2222
import { getStoreCookie, getWithCookie } from '../../utils/cookies'
23+
import type { ProductRating } from './types/ProductRating'
24+
import type {
25+
CreateProductReviewInput,
26+
ProductReviewsInput,
27+
ProductReviewsResult,
28+
} from './types/ProductReview'
29+
import { adaptObject } from '../../utils/adaptObject'
30+
import { camelToSnakeCase } from '../../utils/camelToSnakeCase'
2331

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

@@ -30,6 +38,8 @@ const BASE_INIT = {
3038
},
3139
}
3240

41+
const REVIEWS_AND_RATINGS_API_PATH = 'api/io/reviews-and-ratings/api'
42+
3343
export const VtexCommerce = (
3444
{ account, environment, incrementAddress, subDomainPrefix }: Options,
3545
ctx: Context
@@ -364,5 +374,48 @@ export const VtexCommerce = (
364374
{ storeCookies }
365375
)
366376
},
377+
rating: (productId: string): Promise<ProductRating> => {
378+
return fetchAPI(
379+
`${base}/${REVIEWS_AND_RATINGS_API_PATH}/rating/${productId}`,
380+
undefined,
381+
{ storeCookies }
382+
)
383+
},
384+
reviews: {
385+
create: (input: CreateProductReviewInput): Promise<string> => {
386+
return fetchAPI(
387+
`${base}/${REVIEWS_AND_RATINGS_API_PATH}/review`,
388+
{
389+
...BASE_INIT,
390+
body: JSON.stringify(input),
391+
method: 'POST',
392+
},
393+
{ storeCookies }
394+
)
395+
},
396+
list: ({
397+
orderBy,
398+
orderWay,
399+
...partialInput
400+
}: ProductReviewsInput): Promise<ProductReviewsResult> => {
401+
const formattedInput = adaptObject<string>(
402+
{
403+
orderBy: orderBy ? `${orderBy}:${orderWay ?? 'asc'}` : undefined,
404+
...partialInput,
405+
},
406+
(_, value) => value !== undefined,
407+
camelToSnakeCase,
408+
String
409+
)
410+
411+
const params = new URLSearchParams(formattedInput)
412+
413+
return fetchAPI(
414+
`${base}/${REVIEWS_AND_RATINGS_API_PATH}/reviews?${params.toString()}`,
415+
undefined,
416+
{ storeCookies }
417+
)
418+
},
419+
},
367420
}
368421
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface ProductRating {
2+
average: number
3+
totalCount: number
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
export interface ProductReview {
2+
id: string
3+
productId: string
4+
rating: number
5+
title: string
6+
text: string
7+
reviewerName: string
8+
shopperId: string
9+
reviewDateTime: string
10+
searchDate: string
11+
verifiedPurchaser: boolean
12+
sku: string | null
13+
approved: boolean
14+
location: string | null
15+
locale: string | null
16+
pastReviews: string | null
17+
}
18+
19+
export enum ProductReviewsInputOrderBy {
20+
productId = 'ProductId',
21+
shopperId = 'ShopperId',
22+
approved = 'Approved',
23+
reviewDateTime = 'ReviewDateTime',
24+
searchDate = 'SearchDate',
25+
rating = 'Rating',
26+
locale = 'Locale',
27+
}
28+
29+
export interface ProductReviewsInput {
30+
searchTerm?: string
31+
from?: number
32+
to?: number
33+
orderBy?: ProductReviewsInputOrderBy
34+
orderWay?: 'asc' | 'desc'
35+
status?: boolean
36+
productId?: string
37+
rating?: number
38+
}
39+
40+
export interface ProductReviewsResult {
41+
data: ProductReview[]
42+
range: {
43+
from: number
44+
to: number
45+
total: number
46+
}
47+
}
48+
49+
export interface CreateProductReviewInput {
50+
productId: string
51+
rating: number
52+
title: string
53+
text: string
54+
reviewerName: string
55+
approved: boolean
56+
}

packages/api/src/platforms/vtex/clients/search/types/ProductSearchResult.ts

+4
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ export interface Product {
9393
selectedProperties: Array<{ key: string; value: string }>
9494
releaseDate: string
9595
advertisement?: Advertisement
96+
rating: {
97+
average: number
98+
totalCount: number
99+
}
96100
}
97101

98102
interface Image {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Transforms an object's keys and values based on provided formatters and a predicate filter.
3+
*
4+
* @template T - The type of the transformed values.
5+
* @param obj - The object to transform.
6+
* @param predicate - A predicate function that determines whether a key-value pair should be included in the output.
7+
* @param keyFormatter - A function that formats the object keys. Defaults to returning the key as is.
8+
* @param valueFormatter - A function that formats the object values. Defaults to returning the value as is.
9+
* @returns A new object with transformed keys and values, including only the key-value pairs that satisfy the predicate.
10+
*
11+
* @example <caption>Select all keys that have a defined value and also makes all keys uppercase and all values as numbers</caption>
12+
* ```ts
13+
* const obj = { john: "25", will: "10", bob: undefined };
14+
* const result = adaptObject<number>(
15+
* obj,
16+
* (key, value) => value !== undefined,
17+
* key => key.toUpperCase(),
18+
* Integer.parseInt
19+
* );
20+
* console.log(result); // { JOHN: 25, WILL: 10 }
21+
* ```
22+
*/
23+
export function adaptObject<T>(
24+
obj: Record<string, unknown>,
25+
predicate: (key: string, value: unknown) => boolean,
26+
keyFormatter: (key: string) => string = (key) => key,
27+
valueFormatter: (value: unknown) => T = (value) => value as T
28+
): Record<string, T> {
29+
return Object.entries(obj).reduce(
30+
(acc, [key, value]) => {
31+
if (predicate(key, value)) {
32+
acc[keyFormatter(key)] = valueFormatter(value)
33+
}
34+
35+
return acc
36+
},
37+
{} as Record<string, T>
38+
)
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function camelToSnakeCase(str: string): string {
2+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
3+
}

0 commit comments

Comments
 (0)